Merge remote-tracking branch 'origin/master' into fix/continuable-subagent-policy-inheritance

# Conflicts:
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl
#	examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl
This commit is contained in:
Hypatia May
2026-08-10 22:05:28 +08:00
116 changed files with 230 additions and 3696 deletions

View File

@@ -1,6 +0,0 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.md
2026-07-30-package-manager-native-repository-cache.md: 38e7356d4abfc8eba8854f0a96700da448ff4ac4
2026-07-30-package-manager-native-repository-cache.zh.md: 6833eeb4279c9feb9ac1860e780b8ed58b09d334

View File

@@ -1,47 +0,0 @@
# Agent Note: Package-manager-native repository cache
Status: implemented
English | [中文](2026-07-30-package-manager-native-repository-cache.zh.md)
## Problem
A standalone Harness app cannot rely on a developer-owned SDK project to declare and install repository dependencies. Loading a configured GitHub repository therefore needs a persistent fetch, preparation, and cache boundary, but implementing Git transport, hosted-source syntax, package preparation, and a content store inside DSH would duplicate a package manager. Requiring a separately installed package manager would make a config-only feature depend on host setup.
The cache also needs an update identity. A mutable branch name cannot both remain permanently cached and reflect later commits without an independent refresh protocol.
## Decision
Vendored `@cordisjs/plugin-loader/repository` exports `RepositoryCache`, a generic Node-only package helper with no DSH plugin-format knowledge. Keeping it on a subpath prevents browser consumers of the Loader's main entry from traversing Node filesystem and child-process imports. The caller supplies a package-manager-native source specifier and a cache root. DSH-specific callers own accepted source syntax, path selection, and the cache-root location; the [SDK project dependency workflow](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md#external-cordis-plugin-installation) remains a separate path owned by the developer project's selected package manager.
The Loader carries an exact runtime dependency on `pnpm@11.7.0` and invokes that package's JavaScript entry with the current Node executable. It never discovers a global executable or delegates through Corepack. Each cache miss creates an isolated project with one dependency named `repository`; pnpm owns Git/GitHub resolution, fetching, its content-addressed store, dependency installation, and lifecycle scripts in the repository's dependency graph.
The isolated workspace sets `dangerouslyAllowAllBuilds: true`. A configured repository and its dependency graph are trusted executable code: lifecycle scripts may run before DSH reads any declared assets. The child receives ordinary host process state needed by Git and pnpm, but ambient credential-shaped (`KEY`, `PASSWORD`, `SECRET`, `TOKEN`) variables are removed. No OAuth, token forwarding, or private-repository authentication contract is added.
The SHA-256 of the exact specifier names the cache entry. Concurrent same-process requests share one task. Installation occurs in a sibling temporary directory; only a successful install with a package directory and marker is atomically renamed into the final key. Failed staging is removed, and a competing process's already-published valid entry wins. A later process validates the marker and package directory before returning the stable `node_modules/repository` path.
An identical specifier permanently reuses its published entry. The caller changes the ref or another part of the specifier to request a new generation; the cache does not poll remotes, reinterpret mutable refs, expire entries, or garbage-collect old generations.
## Alternatives considered
**Implement GitHub download, archive extraction, preparation, and caching directly.** Rejected under the [dependency policy](../process/2026-07-26-dependencies-over-hand-rolling.md): pnpm already owns hosted Git syntax, Git execution, lifecycle policy, and a shared content store. A second resolver would add more code while still needing package semantics.
**Require `pnpm` on `PATH` or invoke Corepack.** Rejected because changing one app config must be sufficient on every supported installation. Pinning and shipping the CLI also makes the preparation policy reviewable and independent of the host's package-manager version.
**Resolve a branch or tag again on every startup.** Rejected because it turns startup into a network refresh, changes code without a config diff, and makes rollback depend on remote state. Explicit ref changes preserve auditability even when a user deliberately chooses a mutable ref.
**Disable repository lifecycle scripts.** Rejected because common plugin repositories need a declarative `prepare` step to validate and package their plugin subdirectory. The trust boundary is explicit configuration of executable source, not an incomplete illusion that only static files can run.
**Introduce a Cordis repository service.** Rejected because cache lookup has no runtime contribution registry or provider variation. A small helper lets the later host own Cordis lifecycle and HMR without adding a service contract prematurely.
## Consequences
- Standalone apps carry pnpm's approximately 18.6 MB unpacked runtime instead of requiring a global tool or owning a Git/package implementation.
- A repository author may use ordinary package preparation, and a malicious configured repository or dependency can execute code with the scrubbed child environment and the user's filesystem authority.
- Exact specifiers make startup deterministic after the first successful install; changing cached code requires a config/ref change.
- Failed installs leave no published cache entry and may be retried. Published corruption fails loud instead of silently reinstalling under the same identity.
- Cache generations consume disk until a future explicit cache-management policy removes them.
## Testing
`packages/boot/app-boot/tests/repository-cache.spec.ts` covers same-process single-flight, cross-instance cache reuse, exact-specifier separation, failed-stage cleanup and retry, and boundary validation. Its real local-Git case invokes the bundled pnpm, runs the fixture repository's `prepare` script, and reads the prepared file from the installed cache entry without network access.

View File

@@ -1,47 +0,0 @@
# Agent Note: 包管理器原生仓库缓存
Status: implemented
[English](2026-07-30-package-manager-native-repository-cache.md) | 中文
## 问题
独立运行的 Harness 应用不能依赖开发者自有的 SDK 工程来声明并安装仓库依赖。因此,加载配置中的 GitHub 仓库需要一道持久的获取、准备与缓存边界;但如果在 DSH 内实现 Git 传输、托管来源语法、包准备流程和内容存储,就会重复实现包管理器。若要求用户另行安装包管理器,则只需修改配置即可使用的功能还会依赖宿主环境的额外配置。
缓存还需要明确更新标识。若没有独立的刷新协议,可变分支名无法既永久缓存,又反映后续 commit。
## 决策
vendor 中的 `@cordisjs/plugin-loader/repository` 导出 `RepositoryCache`:一个不包含 DSH 插件格式知识、仅限 Node 使用的通用包辅助工具。把它保留在子路径上,可以避免 Loader 主入口的浏览器消费方在解析依赖时遍历到 Node 文件系统和子进程 import。调用方提供包管理器原生的来源 specifier 和缓存根目录。DSH 专属调用方负责规定可接受的来源语法、路径选择与缓存根目录位置;[SDK 工程依赖工作流](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md#external-cordis-plugin-installation)仍是另一条路径,由开发者工程选定的包管理器负责。
Loader 将 `pnpm@11.7.0` 作为固定版本的运行时依赖,并使用当前 Node 可执行文件调用该包的 JavaScript 入口。它绝不探测全局可执行文件,也不经 Corepack 调用。每次缓存未命中都会创建一个隔离工程,其中只有一个名为 `repository` 的依赖Git 与 GitHub 来源的解析和获取、pnpm 自身的内容寻址 store、依赖安装以及仓库依赖图中的生命周期脚本均由 pnpm 负责。
隔离工作区设置 `dangerouslyAllowAllBuilds: true`。用户配置的仓库及其依赖图都属于受信任的可执行代码DSH 读取任何已声明资产之前,生命周期脚本就可能运行。子进程会收到 Git 与 pnpm 所需的常规宿主进程状态,但会移除环境中名称形似凭据(`KEY``PASSWORD``SECRET``TOKEN`)的变量。该机制不新增 OAuth、token 转发或私有仓库认证约定。
缓存项以精确 specifier 的 SHA-256 命名。同一进程内针对相同 specifier 的并发请求共享一项任务。安装在同级临时目录中进行;只有安装成功且存在包目录和标记时,系统才会把暂存目录原子重命名为最终键对应的目录。失败的暂存目录会被删除;如果另一进程已发布有效项,则以该项为准。后续进程会先校验标记与包目录,再返回稳定的 `node_modules/repository` 路径。
相同的 specifier 会永久复用已发布项。调用方通过修改 ref 或 specifier 的其他部分来请求新的缓存代次;缓存不会轮询远端、重新解释可变 ref、让条目过期也不会垃圾回收旧代次。
## 曾考虑的替代方案
**直接实现 GitHub 下载、归档解压、准备与缓存。** 根据[依赖政策](../process/2026-07-26-dependencies-over-hand-rolling.md)不予采纳pnpm 已负责托管 Git 语法、Git 执行、生命周期政策和共享内容存储。第二套解析器会增加更多代码,却仍需实现包语义。
**要求 `pnpm` 位于 `PATH` 上,或调用 Corepack。** 不予采纳:在每种受支持的安装形态中,只修改一份应用配置就必须足以启用该功能。固定并随应用分发 CLI命令行界面还能使准备政策可供评审并与宿主的包管理器版本无关。
**每次启动都重新解析分支或 tag。** 不予采纳:这会把启动变成网络刷新,在配置 diff 未变化时更改代码,并让回滚依赖远端状态。即使用户有意选择可变 ref显式修改 ref 仍能保持可审计性。
**禁用仓库生命周期脚本。** 不予采纳:常见插件仓库需要声明式 `prepare` 步骤来校验并打包插件子目录。信任边界是显式配置可执行来源,而不是营造一种不完整的假象,仿佛只有静态文件能够运行。
**引入 Cordis 仓库服务。** 不予采纳:缓存查找没有运行时贡献注册表,也不存在提供方变体。小型 helper 让后续宿主负责 Cordis 生命周期与 HMR热模块替换无需过早新增服务约定。
## 后果
- 独立应用随附 pnpm 约 18.6 MB 的解压后运行时,不要求全局工具,也无需自行实现 Git 与包处理。
- 仓库作者可以使用常规包准备流程;恶意的已配置仓库或依赖可以在经过上述清理的子进程环境中,以用户的文件系统权限执行代码。
- 精确 specifier 使首次安装成功后的启动具有确定性;更改缓存代码必须修改配置或 ref。
- 安装失败不会留下已发布缓存项,可以再次重试。已发布缓存损坏时会明确报错,而不会在同一标识下静默重装。
- 缓存代次会持续占用磁盘,直到未来有明确的缓存管理政策将其移除。
## 测试
`packages/boot/app-boot/tests/repository-cache.spec.ts` 覆盖同进程 single-flight、跨实例缓存复用、精确 specifier 隔离、失败暂存清理与重试,以及边界校验。其真实本地 Git 用例会调用随附的 pnpm运行 fixture测试前置数据仓库的 `prepare` 脚本,并在不访问网络的情况下,从已安装缓存项中读取准备后的文件。

View File

@@ -1,6 +0,0 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.md
2026-07-30-static-repository-plugin-format.md: c66ee111eb0cac9e0d6c54581855ffc18efc8611
2026-07-30-static-repository-plugin-format.zh.md: c85aaf44d96098eb1ccb45456cce1a15e7408fc5

View File

@@ -1,49 +0,0 @@
# Agent Note: Static repository Plugin format
Status: implemented
English | [中文](2026-07-30-static-repository-plugin-format.zh.md)
## Problem
A repository that already contains reusable skills or an MCP server declaration should be usable by standalone Harness applications without becoming a Harness SDK project or rewriting its existing layout. Popular repositories must be able to add one `.dsh-plugin` directory while keeping their current skills and `.mcp.json` elsewhere in the tree. These portable static contributions still need to reuse the existing skill and MCP lifecycle owners when the same trusted package also carries native Cordis code.
The [package-manager-native repository cache](2026-07-30-package-manager-native-repository-cache.md) prepares an exact package source but intentionally knows nothing about DSH formats. This layer therefore needs a package-manager-compatible authoring format, a deterministic prepared artifact, and a Cordis composition that stays transactional under Loader disposal and replacement.
## Decision
`@deepseek-ai/dsh-repository-plugin` owns the static contribution subformat inside a `.dsh-plugin` package: skill roots and one common `.mcp.json`. Its package metadata uses `package.json#dsh.skills` for relative skill-root paths and `package.json#dsh.mcpServers` for the relative MCP document path. Each path may leave `.dsh-plugin` to reuse repository content but must remain beneath the directory containing that `.dsh-plugin`; a nested selectable Plugin therefore owns the adjacent subtree above its package without gaining access to unrelated host paths. The package may additionally declare the explicit code entry owned by the [trusted repository package decision](2026-08-08-trusted-repository-package-code.md), and at least one code or static contribution is required.
The `.dsh-plugin` package declares the published `@deepseek-ai/dsh-repository-plugin` package as a development dependency and a non-empty `scripts.prepack` that invokes its `dsh-plugin-prepare` executable. During Git installation, pnpm installs that dependency from the selected package's own manifest; `prepack` runs after dependency installation and before pnpm packs a selected subdirectory, including a Plugin nested inside another package-manager workspace. The package may build its code first. The helper validates metadata and source types, strictly parses `.mcp.json`, copies static assets into `dsh-plugin-assets`, and writes `dsh-plugin.mjs`; the source loader revalidates the installed package's helper-bearing lifecycle metadata before importing that wrapper. A static-only package still receives an import-free wrapper containing its normalized manifest, service-derived `inject` list, and delegation to the `dsh-repository-plugin` Loader builtin. The dependency and workspace-isolation rationale is in the [Git source preparation repair](../bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md).
Loading the DSH package registers that builtin as an effect. A generated wrapper mounts the builtin as its child with `import.meta.url`, so all contributions belong to the wrapper fiber and disappear on Loader removal or rollback. The builtin revalidates the prepared manifest and path containment before reading assets. It composes the existing implementations rather than registering skills or MCP tools itself.
Each prepared skill set mounts `dsh-skill-local` with a unique `repository:<package-name>` provider name, only the copied custom roots, and watching disabled. `dsh-skill-local` therefore gains two general configuration fields: `providerName` and `includeDefaultRoots`. Their defaults preserve its existing single local provider; repository instances set a distinct name and exclude project/user roots so multiple instances neither collide nor duplicate host-local discovery.
Each `.mcp.json` server becomes one existing `dsh-mcp-client` child. The adapter accepts the common root `{ "mcpServers": ... }`; stdio definitions allow only optional `type: "stdio"`, `command`, `args`, and `env`, while HTTP definitions allow only `type: "http"`, `url`, and `headers`. Exact `${NAME}` process-environment references expand at runtime, after cache preparation; missing names fail Plugin load. HTTP maps to the client's Streamable HTTP transport, and stdio uses the prepared package directory as `cwd`. The existing client alone owns connection attempts, failure logging, remote tool synchronization, tool calls, and disconnects. Repository instances enable strict startup, so an initial connection, discovery, or tool-registration failure rejects the repository Loader generation; non-strict standalone clients retain the logged successful-plugin/no-tools behavior.
Unknown MCP fields reject. This intentionally excludes OAuth, `auth` objects, `CLAUDE_PLUGIN_ROOT`, and a broader Claude compatibility contract. Commands, hooks, agents, rules, and other foreign manifest conventions are not inferred from static repository layout; DSH-native behavior uses the explicit trusted Cordis entry. Repository subdirectory selection and GitHub source configuration belong to the [standalone app integration](../feature/2026-07-30-config-only-repository-plugins.md), not this static adapter.
## Alternatives considered
**Discover an entry from `main`, `exports`, or repository layout.** Rejected because static assets do not imply that a package's ordinary entry is a Cordis Plugin. Trusted code loading is explicit through `dsh.entry` and remains outside this static adapter's ownership.
**Teach generated wrappers to implement skills and MCP directly.** Rejected because copied runtime code would drift from `dsh-skill-local` and `dsh-mcp-client`, especially their provider invalidation, tool synchronization, failure, and teardown contracts.
**Import Harness packages from each generated wrapper.** Rejected because repository packages should not resolve or version the application's internal dependency graph. A Loader builtin supplies one app-owned implementation and keeps generated wrappers import-free.
**Watch prepared repository assets.** Rejected because an exact repository cache generation is immutable. Ref, subdirectory, or configuration changes select a new generation; a second watcher would create an unowned refresh identity.
**Make every MCP connect failure a Loader update failure.** Rejected because optional standalone MCP clients deliberately contain startup failures and expose no tools. The MCP client instead owns an explicit strict-startup option, which repository adapters enable for their declared servers.
## Consequences
- Existing skill/MCP repositories can add a small `.dsh-plugin/package.json` without relocating their assets or adopting an SDK project.
- Prepared static output is deterministic glue, while an optional `dsh.entry` and the configured repository lifecycle remain trusted executable package-manager input rather than a sandbox.
- Multiple repository Plugins coexist through provider names and ordinary MCP server-name uniqueness; duplicate names fail through their existing registries and participate in Loader rollback.
- Cached source edits do not appear live. Another exact source/ref/path/config selection is required.
- Adding another portable static contribution kind requires an explicit format and DSH-owned runtime consumer; DSH-native behavior uses the separate explicit code entry.
## Testing
Focused tests prepare skills and MCP metadata, prove a static-only wrapper contains no imports, reject Work IQ-style OAuth fields, map Expo-style HTTP and DataJunction-style stdio plus environment values, and exercise missing variables. A real Loader test mounts a generated wrapper through the registered builtin, reads its skill through `ctx.skills`, removes the Loader entry, and observes provider cleanup. The CI built-entry acceptance invokes `dsh run` with a GitHub source pinned to the pull request head and observes the copied skill alongside the trusted code and MCP proofs owned by the superseding decision.

View File

@@ -1,49 +0,0 @@
# Agent Note: 静态 repository Plugin 格式
状态:已实现
[English](2026-07-30-static-repository-plugin-format.md) | 中文
## 问题
一个已经包含可复用 skills 或 MCP server 声明的仓库,应当能被独立 Harness 应用使用,而不必先变成 Harness SDK 项目,也不应被迫改写现有布局。常见仓库只需新增一个 `.dsh-plugin` 目录,同时仍可把原有 skills 与 `.mcp.json` 放在仓库其他位置。当同一个受信任包还携带原生 Cordis 代码时,这些可移植静态贡献仍需复用现有的 skill 与 MCP 生命周期所有者。
[Package-manager-native repository cache](2026-07-30-package-manager-native-repository-cache.md) 会准备一个精确 package source但有意不了解任何 DSH 格式。因此本层需要一种兼容 package manager 的创作格式、确定性的已准备产物,以及在 Loader dispose 和替换期间仍保持事务性的 Cordis 组合。
## 决策
`@deepseek-ai/dsh-repository-plugin` 负责 `.dsh-plugin` 包内的静态贡献子格式skill 根和一个通用 `.mcp.json`。其包元数据使用 `package.json#dsh.skills` 声明相对 skill 根路径,使用 `package.json#dsh.mcpServers` 声明相对 MCP 文档路径。每条路径都可以离开 `.dsh-plugin` 以复用仓库内容,但必须留在包含该 `.dsh-plugin` 的目录之下;因此,一个嵌套且可选择的插件可以拥有其包上方相邻的子树,却不能访问无关宿主路径。该包还可以声明由[受信任 repository 包决策](2026-08-08-trusted-repository-package-code.md)负责的显式代码入口,并且至少需要一种代码或静态贡献。
`.dsh-plugin` 包将已发布的 `@deepseek-ai/dsh-repository-plugin` 包声明为开发依赖,并声明非空 `scripts.prepack` 来调用其 `dsh-plugin-prepare` 可执行文件。在 Git 安装期间pnpm 会按所选包自身的 manifest元数据清单安装该依赖`prepack` 会在依赖安装后、pnpm 打包选定子目录前运行,即使插件嵌套在另一个包管理器工作区内也不例外。包可以先构建其代码。该辅助程序会校验元数据与源码类型,严格解析 `.mcp.json`,把静态资源复制到 `dsh-plugin-assets`,并写入 `dsh-plugin.mjs`;源码 loader 会在导入该包装层前重新校验已安装包的生命周期元数据是否包含辅助命令。仅含静态贡献的包仍会获得无 import 包装层,其中包含规范化 manifest、由服务派生的 `inject` 列表,以及对 `dsh-repository-plugin` Loader builtin 的委托。依赖与 workspace 隔离的设计依据见[Git 源准备修复](../bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md)。
加载 DSH package 会以 effect 方式注册该 builtin。生成的包装模块使用 `import.meta.url` 把 builtin 挂载为自己的子级,因此所有贡献都归属于包装 fiber并在 Loader 移除或回滚时消失。Builtin 会在读取资源前重新校验已准备 manifest 与路径包含关系。它只组合现有实现,而不自行注册 skills 或 MCP 工具。
每份已准备 skill 集合都会挂载 `dsh-skill-local`,使用唯一的 `repository:<package-name>` 提供方名称、仅包含复制后的自定义根,并禁用监视。因此 `dsh-skill-local` 新增两个通用配置字段:`providerName``includeDefaultRoots`。默认值保持原有单一本地提供方行为repository 实例设置不同名称并排除项目/用户根,使多个实例既不冲突,也不会重复宿主本地发现。
`.mcp.json` 中的每个 server 都变成一个现有 `dsh-mcp-client` 子级。适配层接受通用根对象 `{ "mcpServers": ... }`stdio 定义只允许可选的 `type: "stdio"``command``args``env`HTTP 定义只允许 `type: "http"``url``headers`。严格的 `${NAME}` 进程环境变量引用在运行时、cache 准备之后展开;缺失变量会使 Plugin 加载失败。HTTP 映射到 client 的 Streamable HTTP transportstdio 使用已准备 package 目录作为 `cwd`。只有现有 client 负责连接尝试、失败日志、远端工具同步、工具调用和断开。Repository 实例会启用严格启动,因此初始连接、发现或工具注册失败会拒绝 repository Loader generation非严格的独立 client 则保留“记录日志、Plugin 成功但不注册工具”的行为。
未知 MCP 字段会被拒绝。这里有意排除 OAuth、`auth` 对象、`CLAUDE_PLUGIN_ROOT` 和更广泛的 Claude 兼容约定。命令、hook、agent智能体、规则和其他外来 manifest 约定不会从静态 repository 布局中推断出来DSH 原生行为使用显式的受信任 Cordis 入口。Repository 子目录选择与 GitHub 源配置属于[独立应用集成](../feature/2026-07-30-config-only-repository-plugins.md),而不是本静态适配器。
## 考虑过的替代方案
**从 `main`、`exports` 或 repository 布局中发现入口。** 拒绝,因为静态资源并不表示包的普通入口就是 Cordis 插件。受信任代码通过 `dsh.entry` 显式加载,不属于该静态适配器的职责。
**让生成包装模块直接实现 skills 和 MCP。** 拒绝,因为复制的运行时代码会与 `dsh-skill-local``dsh-mcp-client` 漂移,尤其是提供方失效、工具同步、失败和 teardown 约定。
**让每个生成包装模块 import Harness package。** 拒绝,因为 repository package 不应解析或锁定应用的内部依赖图。Loader builtin 提供一份由 app 所有的实现,并让生成包装模块保持无 import。
**监视已准备 repository 资源。** 拒绝,因为一个精确 repository cache generation 是不可变的。Ref、子目录或配置变化会选择新 generation第二套 watcher 会创造一套没有所有者的刷新身份。
**把每次 MCP 连接失败都当作 Loader 更新失败。** 拒绝,因为可选的独立 MCP client 会有意收束启动失败并且不暴露工具。MCP client 改为自行提供显式的严格启动选项,由 repository 适配器为其声明的 server 启用。
## 后果
- 现有 skillMCP 仓库可以新增一个很小的 `.dsh-plugin/package.json`,无需移动资源或采用 SDK 项目。
- 已准备的静态输出是确定性胶水;可选的 `dsh.entry` 和已配置的 repository 生命周期仍是受信任的可执行包管理器输入,而非沙箱。
- 多个 repository Plugin 通过提供方名称和普通 MCP server-name 唯一性共存;重复名称经现有 registry 失败,并参与 Loader 回滚。
- Cache 内的源码编辑不会实时出现;必须选择另一个精确 sourcerefpathconfig。
- 新增可移植静态贡献类型必须提供显式格式和 DSH 自有运行时消费方DSH 原生行为使用独立的显式代码入口。
## 测试
聚焦测试会准备 skill 与 MCP 元数据,证明仅含静态贡献的包装模块不含 import拒绝 Work IQ 风格的 OAuth 字段,映射 Expo 风格 HTTP 与 DataJunction 风格 stdio 及环境变量,并覆盖缺失变量。真实 Loader 测试通过已注册 builtin 挂载生成包装模块,经 `ctx.skills` 读取其 skill移除 Loader 条目并观察提供方清理。CI 构建入口验收会使用锁定到 PRPull Requesthead 的 GitHub 源调用 `dsh run`,并观察已复制的 skill以及由取代本决策的新决策所负责的受信任代码与 MCP 验证证据。

View File

@@ -1,6 +0,0 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.md
2026-08-08-trusted-repository-package-code.md: 387479b3b36a8bc5e145641ae40802b3090ced70
2026-08-08-trusted-repository-package-code.zh.md: ecc325c3dd0a9e823f1411c3a809c30ada486289

View File

@@ -1,51 +0,0 @@
# Agent Note: Trusted repository packages load Cordis code
Status: implemented
English | [中文](2026-08-08-trusted-repository-package-code.zh.md)
## Problem
The standalone repository format already installs a selected Git package and runs its dependency and lifecycle code with host authority, but it exposed only copied skills and MCP metadata to DSH. Forbidding a Cordis entry did not create a security boundary: package installation remained trusted executable code while the restriction prevented the package from contributing the Plugin behavior that the Harness architecture is designed to compose.
A repository author also needs to keep an ordinary TypeScript npm package shape. Requiring publication to npm, pre-generated JavaScript in Git, or a DSH-owned TypeScript compiler would make a Git source less capable than the same package installed through a developer-owned SDK project. The first model request must observe any MCP tools that this package starts; background-only initial discovery makes a successful installation nondeterministic at the application boundary.
## Decision
A configured repository package is trusted code. Its `.dsh-plugin/package.json` may declare `dsh.entry` as a relative path to a compiled ESM Cordis Plugin inside that package, alongside or instead of `dsh.skills` and `dsh.mcpServers`. At least one contribution is required. The entry may use namespace exports or a default export and retains ordinary Cordis semantics for `name`, `inject`, `Config`, registrations, startup failure, and effect-scoped teardown.
The package owns its npm dependencies and build toolchain. It declares the published `@deepseek-ai/dsh-repository-plugin` package to obtain the `dsh-plugin-prepare` executable. `scripts.prepack` is a non-empty package-authored command that must invoke that dependency-provided helper, but it may first run `tsc`, `tsdown`, or any other build. DSH neither injects the helper, parses the shell program, nor compiles repository source. The helper validates the metadata after the preceding build, requires the configured entry to resolve to a file within `.dsh-plugin`, validates and copies declared static assets, and writes the prepared `dsh-plugin.mjs` wrapper. The installed package must retain a `prepack` declaration containing that helper command; a missing dependency, wrapper, or build output fails before a cache generation becomes usable.
The generated wrapper first mounts the DSH-owned static runtime for skills and MCP definitions, then dynamically imports and unwraps the explicit entry and mounts it as a child. The wrapper statically declares dependencies implied by the prepared manifest; an entry module's additional `inject` is discovered only when mounted and must already be available in the host composition. Both children must reach Cordis `ACTIVE`; an unsatisfied `inject` or startup exception rejects the repository Loader transaction instead of committing an inert generation. Loader removal, failed replacement, and parent disposal unwind the entry, skill providers, MCP clients, and their effects together.
`dsh-mcp-client` resolves its initial connection and tool synchronization promise as part of Plugin application. Its entry is an `async function`, not an ordinary function returning a Promise: Cordis identifies prototype-bearing ordinary functions as constructors and does not treat a constructor's returned Promise as startup work. A valid server's tools therefore exist before its parent repository wrapper activates and before a one-shot application starts its first model request. Its `failOnStartupError` config preserves optional standalone servers by default while letting repository adapters require their declared servers. Repository-translated MCP clients enable that mode, so initial connection, discovery, or tool-registration failure rejects the candidate generation and rollback still closes the transport.
## Trust boundary
Exact refs, source containment, credential-shaped environment scrubbing, prepared manifests, and immutable cache keys protect identity and composition integrity; they do not sandbox executable package input. Repository lifecycle scripts, transitive npm dependencies, the compiled entry, and spawned MCP servers can exercise the authority available to the DSH process and the Cordis services they receive. Users must therefore trust the selected repository and should pin immutable refs and grant Git only the narrow read credential needed for acquisition.
Model-visible behavior remains governed by the owning DSH seam. A repository entry may register tools, prompt sections, policies, commands, agents, or other effects, but anything reaching a model request still needs the corresponding logged DSH representation and lifecycle cleanup. The repository format grants code loading; it does not weaken those service contracts.
## Alternatives considered
**Keep code forbidden while allowing arbitrary package lifecycles.** Rejected because installation already executes trusted repository code, so the restriction added no isolation and forced Plugin authors to publish or maintain a second integration path.
**Have DSH compile repository TypeScript.** Rejected because compiler choice, module layout, generated chunks, native dependencies, and package metadata belong to the npm package. Running the package's declared build preserves the same boundary as other Git dependencies.
**Import `main`, `exports`, or another discovered entry implicitly.** Rejected because an npm package may contain utilities or an MCP executable that is not a Cordis Plugin. The explicit `dsh.entry` field makes code activation reviewable and lets preparation validate the packed path.
**Add a closed manifest field for every future DSH contribution.** Rejected as the universal extension mechanism. Skills and common MCP files retain useful portable static adapters, while DSH-native behavior composes through the existing Cordis Plugin and service contracts.
## Consequences
- A TypeScript DSH Plugin can live in a GitHub repository, install ordinary npm dependencies, compile during `prepack`, and run without publishing the Plugin package to npm.
- Static-only repository packages remain valid and retain import-free wrappers; adding `dsh.entry` opts that package into runtime code import.
- A package build, dependency install, entry import, unmet service, or Plugin startup failure prevents the candidate generation from replacing the last good configuration.
- Initial MCP synchronization can lengthen application startup by the MCP SDK's per-request timeout, and a repository-declared server that is unavailable or cannot publish its complete tool generation prevents that candidate generation from activating.
- Repository code receives host authority, so source review and immutable pinning are operational security requirements rather than optional hardening.
## Testing
Repository-format tests prepare and mount default-export code entries through the real Loader, observe an entry-owned service, remove the Loader row, and observe cleanup; they also retain skill/MCP preparation, containment, damaged-package, pending-service, and rollback coverage. MCP lifecycle tests require `apply` to settle only after initial tool publication, preserve opt-in contained startup failure, and prove strict connection or tool-registration rejection still closes the client.
The Node 24 consumer acceptance uses the actual built `dsh run` command with a fresh DSH home and an authenticated private GitHub source pinned to the pull request's exact head SHA. The test packs the current repository Plugin build with the same private-field removal and workspace-dependency pinning used for publication, serves its packument and tarball from a job-local npm registry, and directs the Git package's ordinary scoped npm resolution there. That repository package obtains `dsh-plugin-prepare` from the simulated published dependency, installs its other pinned runtime and development dependencies, type-checks and bundles TypeScript during `prepack`, prepares a skill plus a stdio MCP server and `dsh.entry`, exposes the skill and MCP schema in the first real model request, executes the MCP tool, and lets the compiled Cordis entry append a second marker to the result observed in the following request. Registry and cache assertions require npm resolution to reach the simulated publication, source files to be absent from the packed installation, and both built modules, their installed dependency, copied assets, and generated wrapper to be present.

View File

@@ -1,51 +0,0 @@
# Agent Note: 受信任 repository 包加载 Cordis 代码
状态:已实现
[English](2026-08-08-trusted-repository-package-code.md) | 中文
## 问题
独立 repository 格式已经会安装选定的 Git 包,并以宿主权限运行其依赖和生命周期代码,但它向 DSH 暴露的只有复制后的 skill技能和 MCP 元数据。禁止 Cordis 入口并未建立安全边界:包安装过程仍会执行受信任代码,而这项限制却阻止包贡献 Harness 架构本就用于组合的插件行为。
仓库作者还需要保持普通 TypeScript NPM 包的结构。如果要求发布到 NPM、把预生成的 JavaScript 签入 Git或使用 DSH 自有的 TypeScript 编译器Git 源的能力就会弱于通过开发者自有 SDK 项目安装的同一个包。首个模型请求必须看到该包启动的所有 MCP 工具;仅在后台进行初始发现,会让一次成功安装在应用边界上具有不确定性。
## 决策
已配置的 repository 包是受信任代码。其 `.dsh-plugin/package.json` 可以连同 `dsh.skills``dsh.mcpServers` 声明 `dsh.entry`,也可以用它取代二者;`dsh.entry` 是指向该包内已编译 ESM Cordis 插件的相对路径。至少需要一种贡献。入口可以使用 namespace 导出或 default export并沿用 Cordis 对 `name``inject``Config`、注册、启动失败和 effect 作用域清理的常规语义。
包自行负责其 NPM 依赖和构建工具链。它声明已发布的 `@deepseek-ai/dsh-repository-plugin` 包以取得 `dsh-plugin-prepare` 可执行文件。`scripts.prepack` 是由包作者编写的非空命令,必须调用该依赖提供的辅助程序,但可以先运行 `tsc``tsdown` 或其他任意构建。DSH 不会注入辅助程序,也不会解析该 shell 程序或编译 repository 源码。辅助程序会在前序构建之后校验元数据,要求已配置入口解析到 `.dsh-plugin` 内的文件,校验并复制已声明的静态资源,再写入已准备的 `dsh-plugin.mjs` 包装层。已安装包必须保留包含该辅助命令的 `prepack` 声明;依赖、包装层或构建输出缺失会在缓存 generation 可用前导致失败。
生成的包装层先挂载 DSH 自有的静态运行时来处理 skill 和 MCP 定义,再动态导入显式入口、解包其导出并将其挂载为子级。包装层会静态声明已准备 manifest元数据清单所隐含的依赖入口模块的额外 `inject` 只有在挂载时才会被发现,并且此时必须已存在于宿主组合中。两个子级都必须进入 Cordis `ACTIVE`;无法满足的 `inject` 或启动异常会拒绝 repository Loader 事务,而不会提交未激活的 generation。Loader 移除、替换失败和父级 dispose资源释放会一并撤销入口、skill 提供方、MCP client 及其 effect。
`dsh-mcp-client` 会在插件应用期间完成其初始连接和工具同步 promise。其入口必须是 `async function`,而不是返回 Promise 的普通函数Cordis 会把带 prototype 的普通函数识别为 constructor不会把 constructor 返回的 Promise 当作启动工作。因此,有效 server 的工具会在父级 repository 包装层激活前、一次性应用发起首个模型请求前就已存在。其 `failOnStartupError` 配置默认保留独立可选 server 的行为,同时允许 repository adapter 要求已声明 server 必须可用。Repository 转换出的 MCP client 会启用该模式,因此初始连接、发现或工具注册失败会拒绝候选 generation回滚仍会关闭 transport。
## 信任边界
精确 ref、源路径包含约束、清除名称符合凭据模式的环境变量、已准备的 manifest 和不可变缓存键可以保护身份与组合完整性它们不会为可执行包输入提供沙箱隔离。Repository 生命周期脚本、传递性 NPM 依赖、已编译入口和 spawn 的 MCP server 可以行使 DSH 进程可用的权限,以及它们所获 Cordis 服务授予的权限。因此,用户必须信任所选仓库,应当固定不可变 ref并只授予 Git 获取源码所需的最小只读凭据。
模型可见行为仍由所属 DSH seam 管理。repository 入口可以注册工具、提示词段落、策略、命令、agent智能体或其他 effect但任何进入模型请求的内容仍须具有对应的 DSH 日志表示和生命周期清理。repository 格式授予代码加载能力;它不会削弱这些服务约定。
## 考虑过的替代方案
**继续禁止代码,但允许任意包生命周期。** 拒绝,因为安装过程本就执行受信任的 repository 代码,所以该限制没有提供隔离,反而迫使插件作者发布或维护第二条集成路径。
**由 DSH 编译 repository TypeScript。** 拒绝,因为编译器选择、模块布局、生成分片、原生依赖和包元数据属于 NPM 包。运行包所声明的构建,可以保持与其他 Git 依赖相同的边界。
**隐式导入 `main`、`exports` 或其他发现的入口。** 拒绝,因为 NPM 包可能包含并非 Cordis 插件的实用工具或 MCP 可执行文件。显式 `dsh.entry` 字段使代码激活可供评审,并让准备阶段校验打包后的路径。
**为未来每种 DSH 贡献添加封闭 manifest 字段。** 不采用它作为通用扩展机制。skill 和通用 MCP 文件仍保留有用的可移植静态适配器DSH 原生行为则通过现有 Cordis 插件与服务约定组合。
## 后果
- TypeScript DSH 插件可以存放在 GitHub 仓库中,安装普通 NPM 依赖,在 `prepack` 期间完成编译,并在无需把插件包发布到 NPM 的情况下运行。
- 仅含静态贡献的 repository 包仍然有效,并保留无 import 包装层;添加 `dsh.entry` 会使该包选择启用运行时代码导入。
- 包构建、依赖安装、入口导入、所需服务未满足或插件启动失败,都会阻止候选 generation 替换最后一个可用配置。
- 初始 MCP 同步可能因 MCP SDK 的单次请求超时而延长应用启动时间repository 声明的 server 不可用或无法发布完整工具 generation 时,该候选 generation 无法激活。
- Repository 代码获得宿主权限,因此源码评审和锁定不可变 ref 是运行安全要求,而不是可选加固措施。
## 测试
repository 格式测试通过真实 Loader 准备并挂载使用 default export 的代码入口,观察入口自有服务,移除 Loader 配置项,再观察清理;测试还保留针对 skillMCP 准备、路径包含约束、包损坏、等待服务和回滚的覆盖。MCP 生命周期测试要求 `apply` 只在初始工具发布后完成,保留可选择启用的启动失败收束行为,并证明严格连接拒绝或工具注册拒绝仍会关闭 client。
Node 24 消费方验收使用实际构建的 `dsh run` 命令、全新 DSH 主目录,以及锁定到 PRPull Request的精确 head SHA 且经过认证的私有 GitHub 源。测试会采用发布时相同的移除 `private` 字段和固定 workspace 依赖版本流程,对当前 repository 插件构建进行打包;再由作业本地 NPM 注册表提供其 `packument` 与 tarball并把 Git 包的常规 scoped NPM 解析指向该注册表。该 repository 包从模拟发布的依赖取得 `dsh-plugin-prepare`,安装其他固定版本的运行时依赖与开发依赖,在 `prepack` 期间对 TypeScript 进行类型检查和打包,准备一个 skill、一个 stdio MCP server 及 `dsh.entry`,在首个真实模型请求中暴露 skill 与 MCP schema执行 MCP 工具,并让已编译 Cordis 入口向结果追加第二个标记,供后续请求观察。注册表与缓存断言要求 NPM 解析必须命中模拟发布,打包安装中不存在源码文件,同时必须存在两个已构建模块、其已安装依赖、复制资源和生成包装层。

View File

@@ -1,6 +0,0 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md
2026-08-08-npm-backed-git-repository-plugin-preparation.md: 958b932f82f4da3cf63aa911260411855e514409
2026-08-08-npm-backed-git-repository-plugin-preparation.zh.md: d2256e0eae303c371371b9b5ba1967105aa61834

View File

@@ -1,48 +0,0 @@
# Agent Note: npm-backed preparation makes GitHub repository Plugins self-contained
Status: implemented
English | [中文](2026-08-08-npm-backed-git-repository-plugin-preparation.zh.md)
## Problem
The repository Plugin authoring contract requires `scripts.prepack` to invoke `dsh-plugin-prepare`. Supplying that executable from the running DSH installation made a source package appear valid even when its own manifest could not obtain the helper. It therefore did not prove the behavior users need after `@deepseek-ai/dsh-repository-plugin` is published: an ordinary Git-hosted npm package must be installable and preparable from only its declared dependencies.
A selectable `.dsh-plugin` inside a pnpm workspace has a second isolation requirement. pnpm prepares a Git-hosted package by running the repository's preferred package manager before packing the selected subdirectory. A nested `pnpm install` can join the containing workspace; when the root lockfile does not list `.dsh-plugin` as an importer, pnpm can report success without installing dependencies declared only by that package. Its TypeScript build or prepare command then fails, or a pre-generated artifact hides the missing dependency.
The checked-in headless fixture mounts an already prepared wrapper. It proves runtime composition, not GitHub acquisition, npm resolution, or package-owned preparation.
## Decision
The `.dsh-plugin` package declares `@deepseek-ai/dsh-repository-plugin` as an ordinary development dependency and invokes its published `dsh-plugin-prepare` executable from `scripts.prepack`. The package may declare any other build and runtime dependencies and run arbitrary compilation before the helper. The repository Plugin package marks its Cordis and DSH peers optional so a helper-only development install resolves only the helper's actual `zod` runtime dependency; an application composition still supplies the peers used by the package's Cordis entry.
DSH does not materialize or prepend a prepare executable. `RepositoryCache` supplies only a transaction-owned `pnpm` wrapper: the outer install runs the pinned pnpm entry directly, while pnpm's hard-coded Git-package `pnpm install` reinvokes the same entry with `--ignore-workspace`. The selected package therefore owns dependency resolution even beneath another pnpm lockfile, and normal package-manager lifecycle `PATH` construction exposes `node_modules/.bin/dsh-plugin-prepare`. The temporary pnpm wrapper disappears after the child settles. The repository remains trusted package-manager input: all dependency and lifecycle code executes under the existing trust contract.
The Node 24 consumer lane passes an exact source derived from the pull request head repository and SHA. It uses the existing private DeepSeek Harness repository rather than creating another repository per run. A job-scoped Git configuration gives the read-only job token access to that exact private source and rewrites pnpm's SSH fallback to authenticated HTTPS.
The built-entry acceptance also creates an in-process npm registry. It stages the current built `@deepseek-ai/dsh-repository-plugin` as a publication artifact by removing `private`, replacing workspace protocols with the release version, and packing the declared files. The registry serves the resulting packument and tarball, while a job-local npm config directs only the `@deepseek-ai` scope to it. The real built `dsh run` child then fetches the exact Git source; that package resolves the helper through npm, type-checks and bundles a TypeScript Cordis entry and MCP server, prepares the adjacent skill, and loads all three contributions. A deliberately failing host `PATH` command proves the lifecycle selected the dependency-local executable. The acceptance also requires registry resolution and inspects the immutable prepared cache, so restoring a host-injected helper cannot satisfy it.
## Alternatives considered
**Inject `dsh-plugin-prepare` from the running DSH installation.** Rejected because it lets an incomplete repository manifest pass and tests a host-only path that npm consumers cannot reproduce.
**Publish the source fixture itself to npm.** Rejected because the product contract is specifically that the DSH Plugin remains Git-hosted; only the reusable preparation helper is an npm dependency.
**Create a new private GitHub repository in every CI run.** Rejected because the pull request repository at its exact head SHA is already a real authenticated private Git remote. Per-run repository mutation would add credentials, cleanup, and eventual-consistency failure modes without changing the acquisition path.
**Prepare after `RepositoryCache` installs the selected package.** Rejected because pnpm's packed subdirectory no longer contains sibling source assets referenced by paths such as `../skills`; preparation must happen before packlist.
**Clone GitHub repositories in DSH and bypass pnpm's Git fetcher.** Rejected because it would duplicate ref resolution, subdirectory selection, dependency installation, packlist behavior, and cache integrity already owned by the pinned package manager.
## Consequences
- A repository author can commit a `.dsh-plugin` package, TypeScript source, skills, and MCP definitions to GitHub without publishing that Plugin package to npm. The package must declare the published preparation dependency.
- Private GitHub sources use the host's standard Git authentication. CI proves that path with a temporary read-only configuration rather than persistent runner credentials.
- `prepack`, not `prepare`, is part of the authoring format. It may contain arbitrary package-owned build steps but must invoke the dependency-provided helper; missing dependency or lifecycle metadata fails before a cache generation is usable.
- A selected package in a pnpm repository installs from its own manifest rather than an enclosing workspace. It cannot rely on workspace-only hoisting; ordinary registry and relative `file:` dependencies remain package-owned inputs.
- Exact source strings identify immutable cache generations; a changed ref or source configuration selects another generation.
- Package dependencies, compilation, preparation, and the trusted `dsh.entry` contribution remain owned by the repository package and the [trusted-code decision](../architecture/2026-08-08-trusted-repository-package-code.md).
## Testing
`packages/boot/app-boot/tests/repository-cache.spec.ts` runs a package excluded from its source repository's root pnpm lockfile through a local Git subpath and requires relative `file:` dependencies to provide both its build command and `dsh-plugin-prepare`; it also proves that visible environment survives while credential-shaped variables are scrubbed. `packages/self-modification/repository-plugin/tests/repository-plugin.spec.ts` pins helper-bearing `prepack` metadata and preparation output. `examples/headless-agent/tests/keyless-smoke.e2e.ts` keeps the checked-in prepared fixture on that source contract. `apps/cli/tests/github-repository-plugin.built.e2e.ts` is the product acceptance: simulated published helper package, job-local npm registry, fresh DSH home, exact authenticated private GitHub source, actual built `dsh run`, package-owned TypeScript build, real MCP execution, code-entry transformation, mock LLM request observation, and prepared cache inspection.

View File

@@ -1,48 +0,0 @@
# Agent Note: 基于 NPM 的准备机制使 GitHub repository 插件自包含
状态:已实现
[English](2026-08-08-npm-backed-git-repository-plugin-preparation.md) | 中文
## 问题
repository 插件创作约定要求 `scripts.prepack` 调用 `dsh-plugin-prepare`。如果由正在运行的 DSH 安装提供该可执行文件,即使源包自身的 manifest元数据清单无法取得辅助程序它也会显得有效。因此这并未证明 `@deepseek-ai/dsh-repository-plugin` 发布后用户所需的行为:普通 Git 托管 NPM 包必须只依靠自身声明的依赖即可安装和准备。
pnpm workspace 内可选择的 `.dsh-plugin` 还有另一项隔离要求。pnpm 会在打包所选子目录前运行仓库首选的包管理器,以准备 Git 托管包。嵌套的 `pnpm install` 可能加入外层 workspace当根 lockfile 未把 `.dsh-plugin` 列为 importer 时pnpm 可能报告成功,却未安装仅由该包声明的依赖。随后,其 TypeScript 构建或准备命令会失败;也可能因为存在预生成产物,依赖缺失被掩盖。
签入仓库的 headless fixture测试前置数据挂载的是已准备好的包装层。它证明运行时组合而不证明 GitHub 获取、NPM 解析或包自有准备。
## 决策
`.dsh-plugin` 包将已发布的 `@deepseek-ai/dsh-repository-plugin` 声明为普通开发依赖,并在 `scripts.prepack` 中调用其已发布的 `dsh-plugin-prepare` 可执行文件。该包可以声明其他任意构建依赖与运行时依赖并在辅助程序前执行任意编译。repository 插件包把 Cordis 与 DSH 对等依赖peer dependency标为可选因此仅为使用辅助程序而进行的开发安装只会解析辅助程序实际依赖的 `zod` 运行时依赖;应用组合仍会提供该包 Cordis 入口所使用的对等依赖。
DSH 不会生成准备阶段可执行文件,也不会将其前置到 `PATH``RepositoryCache` 只提供一个由事务持有的 `pnpm` 包装脚本:外层安装直接运行锁定的 pnpm 入口,而 pnpm 为 Git 包硬编码的 `pnpm install` 会以 `--ignore-workspace` 重新调用同一入口。因此,即使位于另一个 pnpm lockfile 之下,所选包仍自行负责依赖解析,正常的包管理器生命周期 `PATH` 构造会暴露 `node_modules/.bin/dsh-plugin-prepare`。临时 pnpm 包装脚本会在子进程结算后消失。repository 仍是受信任的包管理器输入:所有依赖与生命周期代码都按既有信任约定执行。
Node 24 消费方 CI 任务会传入从 PRPull Requesthead 仓库与 SHA 派生的精确源。它复用现有私有 DeepSeek Harness 仓库,而不会为每次运行新建仓库。作业作用域的 Git 配置允许只读作业 token 访问该精确私有源,并把 pnpm 的 SSH 回退改写为已认证 HTTPS。
构建入口验收还会创建一个进程内 NPM 注册表。它通过移除 `private`、将 workspace protocol 替换为发布版本并打包声明的文件,把当前已构建的 `@deepseek-ai/dsh-repository-plugin` 暂存为发布产物。注册表会提供由此生成的 `packument` 与 tarball作业本地 NPM 配置则只把 `@deepseek-ai` scope 指向它。实际构建的 `dsh run` 子进程随后获取精确 Git 源;该包通过 NPM 解析辅助程序,对 TypeScript Cordis 入口和 MCP server 进行类型检查与打包,准备相邻的 skill技能并加载全部三类贡献。一个刻意设为失败的宿主 `PATH` 命令可以证明,该生命周期选中的是依赖内的可执行文件。验收还要求经过注册表解析并检查不可变的已准备缓存,因此恢复宿主注入的辅助程序也无法通过。
## 考虑过的替代方案
**从正在运行的 DSH 安装注入 `dsh-plugin-prepare`。** 拒绝,因为这会让 manifest 不完整的 repository 包通过,并测试 NPM 消费方无法复现的纯宿主路径。
**把源 fixture 本身发布到 NPM。** 拒绝,因为产品约定明确要求 DSH 插件仍托管在 Git只有可复用的准备辅助程序是 NPM 依赖。
**在每次 CI 运行中创建新的私有 GitHub 仓库。** 拒绝,因为 PR 仓库的精确 head SHA 已是经过认证的真实私有 Git remote。每次运行的仓库变更会增加凭据、清理和最终一致性失败模式却不改变获取路径。
**在 `RepositoryCache` 安装所选包后再准备。** 拒绝,因为 pnpm 打包后的子目录不再包含 `../skills` 等路径所引用的同仓库相邻资源;准备必须在生成 packlist 前完成。
**在 DSH 中克隆 GitHub 仓库并绕过 pnpm 的 Git 获取器。** 拒绝,因为这会重复实现已由锁定包管理器负责的 ref 解析、子目录选择、依赖安装、packlist 行为和缓存完整性。
## 后果
- 仓库作者可以把 `.dsh-plugin` 包、TypeScript 源码、skill 与 MCP 定义提交到 GitHub而无需把该插件包发布到 NPM。该包必须声明已发布的准备依赖。
- 私有 GitHub 源使用宿主的标准 Git 认证。CI 使用临时的只读配置而非运行器上的持久凭据来验证该路径。
- 创作格式使用 `prepack` 而不是 `prepare`。其中可以包含任意包自有构建步骤,但必须调用依赖提供的辅助程序;依赖或生命周期元数据缺失时,会在缓存 generation 可用前失败。
- pnpm 仓库中的所选包按自身 manifest 安装,而不继承外层 workspace。它不能依赖仅由 workspace 提升而可见的包;普通注册表依赖和相对 `file:` 依赖仍是包自有输入。
- 精确源字符串标识不可变缓存 generation改变 ref 或源配置会选择另一个 generation。
- 包依赖、编译、准备和受信任的 `dsh.entry` 贡献仍由 repository 包和[受信任代码决策](../architecture/2026-08-08-trusted-repository-package-code.md)负责。
## 测试
`packages/boot/app-boot/tests/repository-cache.spec.ts` 会通过本地 Git 子路径运行一个未列入源仓库根 pnpm lockfile 的包,并要求相对 `file:` 依赖同时提供构建命令与 `dsh-plugin-prepare`;该测试还证明可见环境变量得以保留,而名称符合凭据模式的变量会被清除。`packages/self-modification/repository-plugin/tests/repository-plugin.spec.ts` 锁定包含辅助命令的 `prepack` 元数据与准备输出。`examples/headless-agent/tests/keyless-smoke.e2e.ts` 使签入仓库的已准备 fixture 继续符合该源格式约定。`apps/cli/tests/github-repository-plugin.built.e2e.ts` 是产品验收测试:模拟发布的辅助程序包、作业本地 NPM 注册表、全新 DSH 主目录、精确且经过认证的私有 GitHub 源、实际构建的 `dsh run`、包自有 TypeScript 构建、真实 MCP 执行、代码入口转换、mock LLM大语言模型请求观测以及已准备缓存检查。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
2026-07-08-self-referential-cordis-toolset.md: 5fc2fb07fcd0b00bf72c818d3806b298312cdc31
2026-07-08-self-referential-cordis-toolset.zh.md: 8f34c97d94cad9b79a0e823406c07cdcfb38793f
2026-07-08-self-referential-cordis-toolset.md: 0d78e0adff487edae00c2422acc1ef8941e7636a
2026-07-08-self-referential-cordis-toolset.zh.md: 665387a9fb24bf0e6fc04fdfb1ced88b932742ad

View File

@@ -40,7 +40,7 @@ The boundary normalizes unambiguous JSON-Schema forms into `ParameterSchemaSpec`
Every temporary Plugin is a child of one internal `cordis-dynamic` group beneath the tool plugin, so ordinary fiber disposal handles toolset reload and unload. `cordis_mount` awaits settlement; startup failure disposes the fiber before returning an error. A settled pending Plugin remains visible with its missing injections. `cordis_unmount` awaits the Plugin fiber's disposal.
Temporary Plugins exist only in process memory. They create no Plugin file, install no package, change no `cordis.yml` or personal/project configuration, do not survive restart, and have no automatic save, promote, or install path. Keeping an experiment means asking the Agent to implement a normal local, project, or repository Plugin through the regular development workflow.
Temporary Plugins exist only in process memory. They create no Plugin file, install no package, change no `cordis.yml` or personal/project configuration, do not survive restart, and have no automatic save, promote, or install path. Keeping an experiment means asking the Agent to implement a normal project Plugin or installable profile bundle through the regular development workflow.
### Cross-mount composition via provide/inject

View File

@@ -40,7 +40,7 @@ vm 隔离了意外的全局污染,上下文门面隐藏了框架内部细节
每个临时 Plugin 都是工具插件下方内部 `cordis-dynamic` 分组的子节点,因此普通的 fiber 释放即可处理工具集重载和卸载。`cordis_mount` 会等待 settlement启动失败时在返回错误前释放 fiber。已 settle 但处于 pending 状态的 Plugin 仍然可见,并列出其缺失的注入。`cordis_unmount` 等待 Plugin fiber 的释放完成。
临时 Plugin 只存在于进程内存中。它不会创建 Plugin 文件、安装 package、修改 `cordis.yml` 或个人/项目配置、跨重启存续,也不存在自动保存、转正式或安装路径。若要保留实验结果,应让 Agent 通过常规开发流程实现普通的本地、项目或仓库 Plugin
临时 Plugin 只存在于进程内存中。它不会创建 Plugin 文件、安装 package、修改 `cordis.yml` 或个人/项目配置、跨重启存续,也不存在自动保存、转正式或安装路径。若要保留实验结果,应让 Agent 通过常规开发流程实现普通的项目 Plugin 或可安装的 profile 组合包
### 通过 provide/inject 实现跨挂载组合

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md
2026-07-20-dsh-cli-personal-config.md: f5207c5ffbd963b9b7c4a7166fa9f17a460707a9
2026-07-20-dsh-cli-personal-config.zh.md: 24478a4b4fd5878032bf80f5b30ab9e2008da785
2026-07-20-dsh-cli-personal-config.md: 02883c89f27e51d6091d4d65167ebdd6a96f6f51
2026-07-20-dsh-cli-personal-config.zh.md: 6e56e892cf682ea514b036750e44dbb06e944a80

View File

@@ -19,7 +19,7 @@ Two coupled pieces, aligned with the `apps/` assembly tier proposed by the `dsh
**Personal config (`dsh-app-boot`).** The personal overlay lives in the Harness home — `$DSH_HOME`, else `~/.dsh` — resolved by the shared [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md) (`@deepseek-ai/dsh-paths`), the same single root skills and AGENTS.md resolve against. The dsh TUI, Web, and headless surfaces consume its two optional files; the demo bins boot their committed trees verbatim:
- `.env` — loaded after the invoking directory's `.env`; `process.loadEnvFile` never overrides, so precedence is ambient > project `.env` > personal `.env`.
- `config.yaml` — a top-level YAML array of `@cordisjs/plugin-include` `PatchOptions`, parsed with the include's own `!!js` dialect (`loadPersonalPatches`) and passed to `boot()`, which forwards it as the root include's `patches`. Patch semantics match the shipped surface overlays: an id-targeted patch replaces the named entry's whole `config`, `insert` appends entries, and an unmatched id is a silent no-op. The [repository Plugin integration](2026-07-30-config-only-repository-plugins.md) uses one shipped row to make an exact GitHub source list a config-only choice.
- `config.yaml` — a top-level YAML array of `@cordisjs/plugin-include` `PatchOptions`, parsed with the include's own `!!js` dialect (`loadPersonalPatches`) and passed to `boot()`, which forwards it as the root include's `patches`. Patch semantics match the shipped surface overlays: an id-targeted patch replaces the named entry's whole `config`, `insert` appends entries, and an unmatched id is a silent no-op. External packages are installed as [profile bundles](../simplification/2026-08-09-remove-repository-plugin.md); this personal layer configures the Loader rows those bundles contribute.
- A missing file means no overlay; a present-but-unreadable, unparsable, or non-array file throws at boot (misconfiguration fails loud, never a silent skip).
The PTY smoke's launcher isolates `$DSH_HOME` to a per-test directory, exactly as it already isolates `DSH_AGENTS_HOME`, so a developer's real personal overlay cannot leak into fixtures; only the dsh CLI reads personal config, so no other test launcher needed changes.
@@ -40,7 +40,7 @@ The TUI and Web register the exact personal path through Cordis HMR after boot.
## Consequences
- `dsh` from any directory (and `pnpm run demo:tui`) can apply personal providers, models, repository Plugins, and other Loader entries with no checkout edit; verified end-to-end against a personal Anthropic proxy with Opus 4.8, including a bash tool round trip.
- `dsh` from any directory (and `pnpm run demo:tui`) can apply personal providers, models, installed bundle entries, and other Loader entries with no checkout edit; verified end-to-end against a personal Anthropic proxy with Opus 4.8, including a bash tool round trip.
- Because an id-targeted patch replaces the whole `config`, a personal override restates the base fields it keeps and can drift when the base entry changes shape; the loader's entry-not-found/name-mismatch warnings and [`dsh --dump-config`](../../../../apps/cli/README.md#profiles) (which prints the composed tree those patches produce) are the diagnostics.
- Personal patches resolve ids against the booted file's own tree, so nested-include overlays (Code Mode) are not personalized; live-run parity for those leaves is deferred.
- `dsh-app-boot` depends on `js-yaml` and imports the include's `!!js` YAML dialect (`entryListSchema`) directly, and, like `apps/cli`, depends on `@deepseek-ai/dsh-paths` for `resolveDshHome`.

View File

@@ -19,7 +19,7 @@ Status: implemented
**个人配置(`dsh-app-boot`)。** 个人 overlay 存放在 Harness home——`$DSH_HOME`,否则 `~/.dsh`——由共享的 [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md)`@deepseek-ai/dsh-paths`)解析,与 skills、AGENTS.md 解析所依据的单一根目录相同。dsh 的 TUI、Web 和无头界面使用其中两个可选文件;各示例 bin 仍然逐字节按已提交的配置树启动:
- `.env`——在调用目录的 `.env` 之后加载;`process.loadEnvFile` 从不覆盖已有值,因此优先级为环境变量 > 项目 `.env` > 个人 `.env`
- `config.yaml`——顶层 YAML 数组,元素为 `@cordisjs/plugin-include``PatchOptions`,用 include 自己的 `!!js` 方言解析(`loadPersonalPatches`)并传给 `boot()`,由它作为根 include 的 `patches` 转发。补丁语义与交付的 surface overlay 一致:按 id 定位的补丁替换该配置项的整个 `config``insert` 追加配置项,未匹配的 id 静默不执行任何操作。[仓库插件集成](2026-07-30-config-only-repository-plugins.md)通过一个已交付配置项,使精确 GitHub 源列表成为纯配置选择
- `config.yaml`——顶层 YAML 数组,元素为 `@cordisjs/plugin-include``PatchOptions`,用 include 自己的 `!!js` 方言解析(`loadPersonalPatches`)并传给 `boot()`,由它作为根 include 的 `patches` 转发。补丁语义与交付的 surface overlay 一致:按 id 定位的补丁替换该配置项的整个 `config``insert` 追加配置项,未匹配的 id 静默不执行任何操作。外部包作为 [profile 组合包](../simplification/2026-08-09-remove-repository-plugin.md)安装;这个个人层负责配置这些组合包提供的 Loader 配置项
- 文件缺失即无 overlay文件存在但不可读、不可解析或非数组则在启动时抛出配置错误响亮失败绝不静默跳过
PTY 冒烟测试的启动器把 `$DSH_HOME` 隔离到每个测试自己的目录,与它已有的 `DSH_AGENTS_HOME` 隔离方式完全一致,开发者真实的个人 overlay 不可能泄漏进 fixture只有 dsh CLI 读取个人配置,因此其他测试启动器无需改动。
@@ -40,7 +40,7 @@ TUI 和 Web 启动后通过 Cordis HMR热模块替换注册确切的个人
## Consequences
- 在任意目录运行 `dsh`(以及 `pnpm run demo:tui`),无需修改 checkout即可应用个人提供方、模型、仓库插件和其他 Loader 配置项;已针对个人 Anthropic 代理与 Opus 4.8 端到端验证,包括一次 bash 工具往返。
- 在任意目录运行 `dsh`(以及 `pnpm run demo:tui`),无需修改 checkout即可应用个人提供方、模型、已安装组合包的配置项和其他 Loader 配置项;已针对个人 Anthropic 代理与 Opus 4.8 端到端验证,包括一次 bash 工具往返。
- 由于按 id 定位的补丁替换整个 `config`,个人覆盖必须复述它保留的基础字段,并可能随基础配置项形态变化而漂移;诊断手段是 loader 的「配置项未找到/名称不匹配」警告和 [`dsh --dump-config`](../../../../apps/cli/README.md#profiles)(打印这些补丁合成出的配置树)。
- 个人补丁只在被启动文件自身的树里解析 id因此嵌套 include 的 overlayCode Mode不会被个性化这些叶子的实际运行等价性暂缓。
- `dsh-app-boot` 依赖 `js-yaml`,并直接导入 include 的 `!!js` YAML 方言(`entryListSchema`);与 `apps/cli` 一样依赖 `@deepseek-ai/dsh-paths` 以获取 `resolveDshHome`

View File

@@ -1,50 +0,0 @@
# Agent Note: Config-only repository Plugins for standalone dsh
Status: implemented
English | [中文](2026-07-30-config-only-repository-plugins.zh.md)
## Problem
A standalone `dsh` user has no developer-owned SDK project whose `package.json`, lockfile, and `cordis.yml` can carry an external Plugin dependency. Requiring an install command or another state file would make “use this repository” a multi-step workflow, while trusted repository code still needs an exact-source, transactional lifecycle owned by the [repository package format](../architecture/2026-08-08-trusted-repository-package-code.md). Long-running TUI and Web processes also need a failed edit to preserve their usable Plugin generation and tell observers why the candidate was rejected.
## Decision
The shipped TUI and Web/headless `cordis.yml` trees contain an empty `repository-plugins` entry. A user changes only `$DSH_HOME/config.yaml`, replacing that entry's config with a `repositories` list. Each item uses `github:owner/repository#<ref>` plus an optional `&path:/.../.dsh-plugin`; omission selects `/.dsh-plugin`. An explicit ref is mandatory, paths are absolute within the repository and end in `.dsh-plugin`, and duplicate normalized specifiers reject before installation. There is no marketplace, discovery index, HTTPS URL vocabulary, or implicit latest generation.
`@deepseek-ai/dsh-repository-plugin` validates and normalizes each source, then resolves it through the generic vendored [`RepositoryCache`](../architecture/2026-07-30-package-manager-native-repository-cache.md). The default cache is `$DSH_HOME/cache/repository-plugins`; `cacheDir` is the explicit deployment override. Bundled pnpm selects the configured repository subpackage, installs its dependencies, runs its package-authored `prepack`, and atomically publishes the exact specifier. The selected package's direct development dependency on `@deepseek-ai/dsh-repository-plugin` supplies `dsh-plugin-prepare` through package-local `node_modules/.bin`; the lifecycle invokes it after any package-owned build. The DSH host imports the generated `dsh-plugin.mjs` wrapper and mounts it as a child fiber; that wrapper composes static skill and MCP owners plus an explicit trusted Cordis entry when declared.
## Live update and failure
`dsh-app-boot` mounts the root Include through one helper that retains its exact Loader `Entry`. The TUI and Web register `$DSH_HOME/config.yaml` through Cordis HMR; headless reads the same file at startup without retaining a watcher. A watcher update rebuilds the Include patch list as immutable app-owned patches followed by the newly parsed personal patches, so Web-generated port, session-root, trust, and frontend values survive every personal edit unless a later personal patch deliberately replaces that row.
Cordis serializes and coalesces exact-path changes. Include and Loader reconcile a candidate transactionally: success commits the new source list, while fetch, preparation, wrapper import, format, or child-Plugin failure rejects the candidate and retains or restores the last good tree. HMR normalizes the caught value to `Error`, logs it, and broadcasts the parallel `hmr/config-update-failed(filename, error)` event; observer failures cannot break refresh processing. Repository MCP servers use strict startup, so an initial connection, discovery, or tool-registration failure rejects the candidate and becomes a config-update failure; non-strict standalone MCP clients retain their contained successful-Plugin/no-tools behavior.
An identical specifier permanently reuses its cache generation. HMR watches configuration, not cached repository code; the user changes the ref, path, or source list to select another generation.
## Trust boundary
Configuring a repository authorizes package-manager lifecycle code, dependencies, the explicit `dsh.entry`, and spawned MCP servers from that repository to run with the user's filesystem authority. The pnpm child removes ambient environment variables whose names contain `KEY`, `PASSWORD`, `SECRET`, or `TOKEN`, but this is credential-exposure reduction rather than a sandbox. The prepared wrapper validates composition boundaries and lifecycle state; it does not make repository code safe to run when the source is untrusted.
## Alternatives considered
**Require an SDK project dependency.** Rejected for the standalone app path because there is no project manifest to edit. Developer-owned SDK projects keep their native package-manager workflow as a separate capability.
**Add a `dsh plugin install` command and installation database.** Rejected because the personal Loader overlay already owns machine-local composition. A second mutation interface and durable registry would duplicate config identity and rollback.
**Resolve repositories directly in the DSH package.** Rejected because Git transport, GitHub subpackage selection, lifecycle execution, and content storage belong to pnpm and the generic Loader cache, not a DSH-specific adapter.
**Watch cache contents or refresh the same ref automatically.** Rejected because one config value must identify one immutable prepared generation. Background remote resolution would change executable code without a config diff and make rollback depend on mutable remote state.
**Broadcast an `unknown` failure payload.** Rejected at the HMR boundary. JavaScript may throw any value internally, but the public event always receives a normalized `Error`, giving observers one stable contract while retaining the original value as its cause when needed.
## Consequences
- A repository that adds `.dsh-plugin/package.json` can reach standalone users through one personal-config edit without changing its existing skills or `.mcp.json` layout.
- Long-running apps can add, replace, or remove configured generations without restart; rejected candidates retain the last good runtime and produce one generic Cordis event.
- First use may require Git/network access and preparation time. Later starts reuse the exact prepared cache; old generations consume disk until a separate cache-management policy exists.
- Skills and common MCP definitions retain portable static adapters, while an explicit `dsh.entry` can contribute DSH-native Cordis behavior. Format-specific compatibility shims, OAuth-bearing MCP definitions, and marketplaces remain intentionally absent.
## Testing
Repository-package tests pin source normalization, default and nested `.dsh-plugin` paths, cache-root resolution, duplicate rejection, prepared-wrapper loading, and disposal. App-boot tests drive exact-path add, two failure classes, recovery, removal, failure events, and generated-patch preservation through the real HMR/Include/Loader path. A keyless PTY smoke boots the shipped `dsh` composition from personal config alone and invokes a skill from a seeded immutable cache generation.

View File

@@ -1,50 +0,0 @@
# Agent Note: 仅凭配置为独立 dsh 接入仓库插件
Status: implemented
[English](2026-07-30-config-only-repository-plugins.md) | 中文
## 问题
独立 `dsh` 用户没有开发者自有的 SDK 项目,无法由其 `package.json`、lockfile 和 `cordis.yml` 承载外部插件依赖。若要求运行安装命令或维护另一份状态文件,「使用这个仓库」就会变成多步骤流程;受信任的 repository 代码仍需要由[repository 包格式](../architecture/2026-08-08-trusted-repository-package-code.md)负责一套锁定精确来源且具事务性的生命周期。长时间运行的 TUI 和 Web 进程还必须在编辑失败时保留仍可使用的插件版本,并向观察者说明候选配置被拒绝的原因。
## 决策
已交付的 TUI 和 Web无头 `cordis.yml` 配置树包含一个空的 `repository-plugins` 配置项。用户只需修改 `$DSH_HOME/config.yaml`,用 `repositories` 列表替换该配置项的配置。每一项采用 `github:owner/repository#<ref>`,并可追加 `&path:/.../.dsh-plugin`;省略时选择 `/.dsh-plugin`。必须显式指定 ref路径是仓库内的绝对路径并以 `.dsh-plugin` 结尾重复的规范化说明符在安装前即被拒绝。不提供插件市场、发现索引、HTTPS URL 词汇或隐式的最新版本。
`@deepseek-ai/dsh-repository-plugin` 校验并规范化每个源,再通过 vendor 中的通用 [`RepositoryCache`](../architecture/2026-07-30-package-manager-native-repository-cache.md) 解析。默认缓存位于 `$DSH_HOME/cache/repository-plugins``cacheDir` 是显式的部署覆盖项。随应用提供的 pnpm 选择已配置的 repository 子包,安装其依赖,运行包所定义的 `prepack`,并原子发布该精确说明符。所选包对 `@deepseek-ai/dsh-repository-plugin` 的直接开发依赖通过包内 `node_modules/.bin` 提供 `dsh-plugin-prepare`该生命周期会在任何包自有构建完成后调用它。DSH 宿主会导入生成的 `dsh-plugin.mjs` 包装层并将其挂载为子 fiber该包装层组合静态 skill技能与 MCP 所有者,并在声明时组合显式的受信任 Cordis 入口。
## 实时更新与失败
`dsh-app-boot` 通过一个辅助函数挂载根 Include并保留其确切的 Loader `Entry`。TUI 和 Web 通过 Cordis HMR热模块替换注册 `$DSH_HOME/config.yaml`;无头模式在启动时读取同一文件,但不保留监视器。监视器更新会重新构建 Include 补丁列表先放置不可变的应用自有补丁再放置新解析的个人补丁。因此Web 生成的端口、会话根目录、信任和前端值会在每次个人编辑后保留,除非后续个人补丁有意替换相应配置项。
Cordis 会串行处理并合并该确切路径上的变更。Include 与 Loader 以事务方式协调候选配置成功时提交新源列表拉取、准备、包装模块导入、格式或子插件失败时拒绝候选配置并保留或恢复最后一个可用树。HMR 会把捕获的值规范化为 `Error`,记录错误,并广播并行的 `hmr/config-update-failed(filename, error)` 事件观察者失败不会中断刷新处理。Repository MCP 服务器采用严格启动,因此初始连接、发现或工具注册失败会拒绝候选配置,并构成配置更新失败;非严格的独立 MCP 客户端仍保留其所收束的「插件成功加载但无工具」行为。
相同说明符会永久复用同一个缓存版本。HMR 监视配置,而非已缓存的仓库代码;用户必须改变 ref、路径或源列表才能选择另一个版本。
## 信任边界
配置仓库即授权该仓库中的包管理器生命周期代码、依赖、显式 `dsh.entry` 和 spawn 的 MCP server 以用户的文件系统权限运行。pnpm 子进程会移除名称中含有 `KEY``PASSWORD``SECRET``TOKEN` 的环境变量,但这只会减少凭据暴露,并非沙箱。已准备的包装层会校验组合边界和生命周期状态;当来源不受信任时,它无法让 repository 代码变得可安全运行。
## 考虑过的替代方案
**要求声明 SDK 项目依赖。** 独立应用路径没有可编辑的项目 manifest元数据清单因此否决。开发者自有的 SDK 项目仍可使用原生包管理器工作流,这是一项独立能力。
**新增 `dsh plugin install` 命令和安装数据库。** 否决,因为个人 Loader 覆盖层已经负责机器本地组合。第二个变更接口和持久注册表会重复配置身份与回滚机制。
**由 DSH 包直接解析仓库。** 否决,因为 Git 传输、GitHub 子包选择、生命周期执行和内容存储属于 pnpm 与通用 Loader 缓存,而非 DSH 专用适配器。
**监视缓存内容,或自动刷新相同 ref。** 否决,因为一个配置值必须标识一个不可变的已准备版本。后台远端解析会在没有配置差异的情况下改变可执行代码,并使回滚依赖可变的远端状态。
**广播 `unknown` 失败载荷。** 在 HMR 边界否决。JavaScript 内部可以抛出任意值,但公开事件始终接收规范化的 `Error`,从而为观察者提供稳定约定,并在需要时把原始值保留为错误原因。
## 后果
- 添加 `.dsh-plugin/package.json` 的仓库只需一次个人配置编辑即可供独立用户使用,无需改变现有 skill 或 `.mcp.json` 布局。
- 长时间运行的应用无需重启即可新增、替换或移除已配置版本;被拒绝的候选配置会保留最后一个可用运行时,并产生一个通用 Cordis 事件。
- 首次使用可能需要 Git网络访问和准备时间。后续启动会复用这份精确的已准备缓存在另行制定缓存管理政策之前旧版本会持续占用磁盘空间。
- skill 和通用 MCP 定义保留可移植静态适配器,而显式 `dsh.entry` 可以贡献 DSH 原生 Cordis 行为。格式专用的兼容 shim、带 OAuth 的 MCP 定义和插件市场仍有意不提供。
## 测试
仓库包测试固定源规范化、默认和嵌套 `.dsh-plugin` 路径、缓存根解析、重复项拒绝、已准备包装层加载及资源释放。App-boot 测试通过真实 HMRIncludeLoader 路径驱动确切路径的新增、两类失败、恢复、移除、失败事件及生成补丁保留。一个无密钥 PTY 冒烟测试仅通过个人配置启动已交付的 `dsh` 组合,并从预置的不可变缓存版本中调用一个 skill。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.md
2026-07-30-config-only-repository-plugins.md: 35327a30e03c51311f634e05ade209ab93ae0155
2026-07-30-config-only-repository-plugins.zh.md: 5755045560da761b59f7c65e99d551f599c2b5b3
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-09-remove-repository-plugin.md
2026-08-09-remove-repository-plugin.md: 8ac6fd18b756e227f8dabc82eb4926a51702c5a1
2026-08-09-remove-repository-plugin.zh.md: 6e504a151a88b87b8093a91a91ede1cb919a0b45

View File

@@ -0,0 +1,44 @@
# Agent Note: Remove the dedicated repository Plugin path
Status: implemented
English | [中文](2026-08-09-remove-repository-plugin.zh.md)
## Problem
The repository Plugin path duplicated the profile bundle path for installing and composing third-party packages. It added a `.dsh-plugin` manifest, a generated wrapper, a preparation executable, a second Git/package cache, a Loader builtin, and repository-specific Skill and MCP adapters. Profile bundles already install npm or Git package specifications through the profile package manager, retain normal dependency and lifecycle semantics, and contribute an ordered `cordis.patch.yml` layer that can mount ordinary Cordis Plugins.
The duplicate path also exposed less configuration than a bundle. Its `repositories` list selected source strings, but the generated wrapper mounted a code entry without a user-supplied Plugin config. Repository-specific preparation therefore added substantial code and CI work without becoming the general external-Plugin distribution mechanism.
## Decision
DeepSeek Harness has one standalone external-Plugin distribution path: installable profile bundles. `dsh plugin --profile <name> add <package-or-git-spec>` records the dependency in the profile package, and the installed package declares `dsh.bundle.patch` to contribute its patch layer. The package manager owns source acquisition, versions, dependencies, build lifecycles, and its lockfile. The bundle patch owns Cordis Plugin selection and complete Plugin config.
The `@deepseek-ai/dsh-repository-plugin` package, `.dsh-plugin` authoring format, `dsh-plugin-prepare` executable, generated wrapper, immutable repository cache, base `repository-plugins` row, and dedicated GitHub acceptance lane are removed. The unused vendored `@cordisjs/plugin-loader/repository` subpath and its bundled pnpm dependency are removed with their only consumer. Existing repository cache directories are inert user data; DSH neither reads nor deletes them.
Bundles compose existing owners directly. A bundle that contributes Skills mounts `@deepseek-ai/dsh-skill-local`; one that contributes MCP servers mounts `@deepseek-ai/dsh-mcp-client`; native behavior mounts an ordinary compiled Cordis Plugin. These packages retain their own validation, lifecycle, registration, and teardown contracts. No compatibility parser or migration from `.dsh-plugin` is retained under the pre-release compatibility policy.
This note consolidates the removed repository cache, static format, config-only integration, npm-backed preparation, and trusted code-entry decisions. Their original motivation survives here: standalone users need package-manager-owned external composition, Git and npm dependencies may execute trusted lifecycle code, static Skill and MCP contributions should reuse their existing owners, and source identity belongs in the profile dependency specification and lockfile. Their implementation-specific wrappers, cache generations, and preparation protocol no longer constrain the product.
## Alternatives considered
**Keep repository Plugin as a convenience wrapper over bundles.** Rejected because it would preserve two install commands, two manifest formats, and two failure/cache identities for the same package. A convenience that cannot pass ordinary Plugin config also remains less capable than the mechanism it wraps.
**Teach the repository wrapper to load a bundle patch.** Rejected because the repository cache and preparation protocol would still duplicate profile dependency installation. Bundle packages are already accepted from npm, Git, file, and link specifications through pnpm.
**Keep the generic Loader repository cache for possible future consumers.** Rejected because it has no current consumer after the package removal and carries a pinned package-manager runtime in a vendored browser-adjacent package. A dedicated cache is warranted again only if configuration-time activation without an explicit installation becomes a product requirement that profile dependencies cannot satisfy; that consumer can choose its cache contract then.
**Disable repository Plugin but retain its on-disk format for migration.** Rejected under the pre-release stance. Retaining a parser or compatibility loader would keep the removed contract alive without an external compatibility obligation.
## Consequences
- Third-party packages use one installation and composition model, with ordinary dependency declarations and full patch-level Plugin config.
- Installing or updating an external bundle is an explicit `dsh plugin` package-manager operation rather than a watched source-list edit. User patch HMR still configures rows contributed by installed bundles.
- Profile installation requires `pnpm` on the host `PATH`. This is acceptable for an explicit package-management operation and avoids shipping the removed cache's pinned package-manager runtime solely for configuration-time activation.
- `.dsh-plugin` packages and existing repository source-list patches stop working. Their cache files remain removable by the user but are not migrated or automatically deleted.
- The dedicated pnpm runtime, preparation executable, wrapper generator, Git credential CI setup, repository cache, and repository-specific tests disappear.
- Package-relative static assets need a bundle-owned path form so a declarative bundle can point `dsh-skill-local`, `dsh-mcp-client`, or another Plugin at files it ships without custom runtime glue. That capability is owned by the bundle format rather than a repository adapter.
## Testing
Static gates reject stale package, config, documentation, graph, and workspace references. The existing `dsh plugin` built-CLI acceptance covers profile initialization, package-manager installation, bundle discovery, and layer reconciliation. Declarative package-relative Skill and MCP bundle resources remain a named coverage gap in this removal layer.

View File

@@ -0,0 +1,44 @@
# Agent Note: 移除专用 repository 插件路径
Status: implemented
[English](2026-08-09-remove-repository-plugin.md) | 中文
## 问题
repository 插件路径与 profile 组合包路径重复实现了第三方包的安装和组合。它增加了 `.dsh-plugin` manifest元数据清单、生成的包装层、准备工作可执行文件、第二套 Git包缓存、Loader 内置项,以及 repository 专用的 skill技能和 MCP 适配器。profile 组合包已经能通过 profile 包管理器安装 npm 或 Git 包说明符,保留正常的依赖与生命周期语义,并提供一个有序 `cordis.patch.yml` 层,其中可以挂载普通 Cordis 插件。
重复的路径所能提供的配置也少于组合包。其 `repositories` 列表选择源字符串但生成的包装层挂载代码入口时无法传入用户提供的插件配置。因此repository 专用的准备流程增加了大量代码和 CI 工作,却没有成为通用的外部插件分发机制。
## 决策
DeepSeek Harness 只保留一种独立的外部插件分发路径:可安装的 profile 组合包。`dsh plugin --profile <name> add <package-or-git-spec>` 将依赖记录到 profile 包中,安装的包通过声明 `dsh.bundle.patch` 提供自己的 patch 层。包管理器负责获取源、管理版本和依赖、运行构建生命周期,并维护锁文件。组合包 patch 负责选择 Cordis 插件并提供完整的插件配置。
移除 `@deepseek-ai/dsh-repository-plugin` 包、`.dsh-plugin` 编写格式、`dsh-plugin-prepare` 可执行文件、生成的包装层、不可变 repository 缓存、base 中的 `repository-plugins` 配置项,以及专用 GitHub 验收流水线。vendor 中未再使用的 `@cordisjs/plugin-loader/repository` 子路径及其随附的 pnpm 依赖,也随唯一消费方一并移除。现有 repository 缓存目录只是不会再产生作用的用户数据DSH 既不会读取,也不会删除这些目录。
组合包直接组合现有归属方。提供 skill 的组合包挂载 `@deepseek-ai/dsh-skill-local`;提供 MCP 服务器的组合包挂载 `@deepseek-ai/dsh-mcp-client`;原生行为则挂载普通的已编译 Cordis 插件。这些包继续保有各自的校验、生命周期、注册和 teardown 契约。根据预发布兼容政策,不保留针对 `.dsh-plugin` 的兼容解析器或迁移机制。
本说明整合了已移除的 repository 缓存、静态格式、纯配置集成、由 npm 支持的准备流程和受信任代码入口决策。其原始动机保留于此独立用户需要由包管理器负责的外部组合方式Git 和 npm 依赖可以执行受信任的生命周期代码;静态 skill 与 MCP 贡献应复用现有归属方;来源标识应位于 profile 的依赖说明符和锁文件中。相应实现特有的包装层、缓存 generation 和准备协议不再约束产品。
## 曾考虑的替代方案
**保留 repository 插件,将其作为组合包的便利包装层。** 不予采纳,因为这会为同一个包保留两条安装命令、两种 manifest 格式,以及两套失败/缓存标识。如果一层便利包装不能传递普通的插件配置,其能力仍然不及它所包装的机制。
**让 repository 包装层加载组合包 patch。** 不予采纳,因为 repository 缓存和准备协议仍会重复 profile 依赖安装。组合包已经可以通过 pnpm 接受 npm、Git、file 和 link 说明符。
**为未来可能出现的消费方保留通用 Loader repository 缓存。** 不予采纳,因为在移除相关包后,它已无当前消费方,却仍让一个 vendor 中与浏览器相邻的包携带固定版本的包管理器运行时。只有当无需显式安装即可在配置阶段激活这一能力成为 profile 依赖无法满足的产品需求时,才有理由重新引入专用缓存;届时该消费方可以选择自己的缓存约定。
**禁用 repository 插件,但保留其磁盘格式以供迁移。** 根据预发布方针,不予采纳。保留解析器或兼容 loader 会在没有外部兼容义务的情况下,让已移除的契约继续存在。
## 后果
- 第三方包统一使用一种安装与组合模型,采用普通依赖声明和完整的 patch 层插件配置。
- 安装或更新外部组合包时,必须显式通过 `dsh plugin` 执行包管理器操作,而不是编辑受监听的源列表。用户 patch 的 HMR热模块替换仍可配置已安装组合包所提供的配置项。
- 安装 profile 时,宿主机的 `PATH` 中必须提供 `pnpm`。对于显式的包管理操作,这一要求可以接受,并且可避免仅为配置阶段激活而随产品交付已移除缓存所使用的固定版本包管理器运行时。
- `.dsh-plugin` 包和现有 repository 源列表 patch 停止工作。用户仍可自行删除其缓存文件,但系统不会迁移或自动删除这些文件。
- 专用 pnpm 运行时、准备工作可执行文件、包装层生成器、Git 凭据 CI 设置、repository 缓存和 repository 专用测试全部消失。
- 静态资源需要一种由组合包拥有、可相对于包解析的路径形式,使声明式组合包可以将 `dsh-skill-local``dsh-mcp-client` 或其他插件指向它随包交付的文件,而无需定制运行时代码。该能力归组合包格式所有,而不是 repository 适配器。
## 测试
静态门禁会拒绝残留的包、配置、文档、图和 workspace 引用。现有 `dsh plugin` 已构建 CLI命令行界面验收测试覆盖 profile 初始化、包管理器安装、组合包发现和层调和。声明式、相对于包解析的 skill 与 MCP 组合包资源仍是本移除层中已明确记录的覆盖缺口。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md
2026-07-17-sdk-follow-up-capabilities.md: 9a17f07139014f95666789e41cacb7af180ef5d8
2026-07-17-sdk-follow-up-capabilities.zh.md: 2431c5c3d9e45605223d9bb1843e48a3c711759e
2026-07-17-sdk-follow-up-capabilities.md: f14d46a61f5fd3e64067441c2f8340cf94746a79
2026-07-17-sdk-follow-up-capabilities.zh.md: 2e5efba340d503d2445e408bfc43ee0d6c6bec3c

View File

@@ -47,7 +47,7 @@ The repository ships a thin `SKILL.md` that teaches an agent to construct the st
The package manager owns source parsing, version or commit resolution, integrity data, lockfile updates, and any build policy. The SDK does not download or unpack a second copy through giget or pacote. An external plugin remains a dependency under `node_modules`; local plugin scaffolding remains a separate project-creation concern.
This proposal concerns dependencies of developer-owned SDK projects. Standalone app repository caching, its bundled-pnpm policy, and its explicit preparation trust boundary are owned by the [package-manager-native repository cache](../../implemented/architecture/2026-07-30-package-manager-native-repository-cache.md).
This proposal concerns dependencies of developer-owned SDK projects. Standalone apps install external packages as [profile bundles](../../implemented/simplification/2026-08-09-remove-repository-plugin.md), with their profile package manager and lockfile owning acquisition and lifecycle policy.
## Launcher telemetry

View File

@@ -47,7 +47,7 @@ Create 和 config 使用相同的功能计划形状。create 通过上述命令
包管理器负责来源解析、版本或 commit 解析、`integrity` 数据、lockfile 更新和构建策略。SDK 不再通过 giget 或 pacote 下载、解压第二份副本。外部插件是 `node_modules` 下的依赖;本地插件脚手架仍属于独立的工程创建问题。
本提案只涉及开发者自有 SDK 工程的依赖。独立应用的仓库缓存、随应用捆绑 pnpm 的政策和显式的准备流程信任边界,均由[包管理器原生仓库缓存](../../implemented/architecture/2026-07-30-package-manager-native-repository-cache.md)负责
本提案只涉及开发者自有 SDK 工程的依赖。独立应用将外部包安装为 [profile 组合包](../../implemented/simplification/2026-08-09-remove-repository-plugin.md),由 profile 的包管理器与 lockfile 负责获取和生命周期策略
## Launcher 遥测

View File

@@ -175,8 +175,6 @@ jobs:
DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
DSH_OXLINT_THREADS: '8'
DSH_PUBLINT_CONCURRENCY: '8'
DSH_GITHUB_REPOSITORY_PLUGIN_SOURCE: >-
github:${{ github.event.pull_request.head.repo.full_name }}#${{ github.event.pull_request.head.sha }}&path:/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin
# Failover halves snapshot concurrency for the shared 64-core VM.
DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '12' || '32' }}
steps:
@@ -242,17 +240,6 @@ jobs:
if: vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]'
run: pnpm --filter @deepseek-ai/dsh-frontend exec playwright install chromium
- name: Configure private GitHub repository Plugin access
env:
DSH_GITHUB_SOURCE_TOKEN: ${{ github.token }}
run: |
source_config="$RUNNER_TEMP/dsh-github-source.gitconfig"
basic_auth=$(printf 'x-access-token:%s' "$DSH_GITHUB_SOURCE_TOKEN" | base64 | tr -d '\n')
git config --file "$source_config" url.https://github.com/.insteadOf git@github.com:
git config --file "$source_config" --add url.https://github.com/.insteadOf ssh://git@github.com/
git config --file "$source_config" http.https://github.com/.extraheader "AUTHORIZATION: basic $basic_auth"
echo "GIT_CONFIG_GLOBAL=$source_config" >> "$GITHUB_ENV"
- name: Run compatibility, snapshot, and artifact gates
run: pnpm run check:ci:consumers

View File

@@ -80,7 +80,6 @@ External packages that a workspace package resolves at runtime. `scripts/install
| [`node-addon-require-builtin`](https://www.npmjs.com/package/node-addon-require-builtin) | MIT |
| [`node-pty`](https://github.com/microsoft/node-pty) | MIT |
| [`picomatch`](https://github.com/micromatch/picomatch) | MIT |
| [`pnpm`](https://github.com/pnpm/pnpm) | MIT |
| [`react`](https://github.com/facebook/react) | MIT |
| [`react-dom`](https://github.com/facebook/react) | MIT |
| [`sharp`](https://github.com/lovell/sharp) | Apache-2.0 |

View File

@@ -12,8 +12,6 @@ flowchart LR
cfg --> plugin_dsh_base_timer
plugin_dsh_base_hmr["hmr<br/>@cordisjs/plugin-hmr"]
cfg --> plugin_dsh_base_hmr
plugin_dsh_base_repository_plugins["repository-plugins<br/>@deepseek-ai/dsh-repository-plugin"]
cfg --> plugin_dsh_base_repository_plugins
plugin_dsh_base_llm["llm<br/>@deepseek-ai/dsh-llm"]
cfg --> plugin_dsh_base_llm
plugin_dsh_base_session["session<br/>@deepseek-ai/dsh-session"]
@@ -168,7 +166,6 @@ flowchart LR
| --- | --- |
| `timer` | `@cordisjs/plugin-timer` |
| `hmr` | `@cordisjs/plugin-hmr` |
| `repository-plugins` | `@deepseek-ai/dsh-repository-plugin` |
| `llm` | `@deepseek-ai/dsh-llm` |
| `session` | `@deepseek-ai/dsh-session` |
| `typert` | `@deepseek-ai/dsh-typert-registry` |

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md
README.md: 0b5faf8993cd8065fffcfec5f240b0084508db91
README.zh.md: b9c48c16dd4be186266d30a438329463c31aca70
README.md: 7f0bd7b0bd50482b3ba7ee95adf6aaf1defb6d28
README.zh.md: 55f23c5644b8b063ef12fb56fa587ff5d2eb21a2

View File

@@ -65,11 +65,11 @@ New sessions default to the `workspace-write` permission preset. Bash and filesy
## Shared deployment behavior
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, repository Plugin support, and session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`; `web_fetch` is disabled unless a patch layer inserts a provider and enables it.
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, and session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`; `web_fetch` is disabled unless a patch layer inserts a provider and enables it.
Session events stream as OTLP/HTTP logs by default. `DSH_TELEMETRY_OTLP_URL` selects another collector. Any non-empty `DSH_TELEMETRY_DISABLED` disables the telemetry row before boot. The shipped base has no telemetry redaction rule, so exported records can contain message text, tool arguments and results, and workspace paths; the [telemetry Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md) owns that deployment decision.
The empty `repository-plugins` row lets profile patch layers mount prepared immutable repository Plugin generations. See the [repository Plugin contract](../../../packages/self-modification/repository-plugin/README.md#standalone-app-configuration). The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.
Install external plugin bundles through `dsh plugin --profile <name> add <package-or-git-spec>`. The installed package owns its dependencies and contributes its declared `cordis.patch.yml` layer. The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.
## Source launcher

View File

@@ -65,11 +65,11 @@ dsh web --dump-config
## 共享部署行为
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search`、repository Plugin 支持和会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env``$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;只有 patch 层插入提供方并启用 `web_fetch` 后,该工具才可用。
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env``$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;只有 patch 层插入提供方并启用 `web_fetch` 后,该工具才可用。
会话事件默认作为 OTLP/HTTP 日志流式发送。`DSH_TELEMETRY_OTLP_URL` 选择其他 collector。任何非空 `DSH_TELEMETRY_DISABLED` 都会在启动前禁用遥测配置行。随附基础配置没有遥测脱敏规则,因此导出的记录可能包含消息文本、工具参数与结果以及 workspace 路径;该部署决策由[遥测 Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md)负责。
`repository-plugins` 行让 profile 的 patch 层能够挂载已准备的不可变 repository Plugin generation。参见 [repository Plugin 约定](../../../packages/self-modification/repository-plugin/README.md#standalone-app-configuration)。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent智能体沙箱之外的受信任可执行代码。
通过 `dsh plugin --profile <name> add <package-or-git-spec>` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml`。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent智能体沙箱之外的受信任可执行代码。
## 源码启动器

View File

@@ -1,10 +0,0 @@
{
"mcpServers": {
"github_repository": {
"command": "node",
"args": [
"lib/mcp-server.mjs"
]
}
}
}

View File

@@ -1,30 +0,0 @@
{
"name": "dsh-github-repository-plugin-e2e-fixture",
"version": "0.0.0",
"private": true,
"type": "module",
"files": [
"lib",
"dsh-plugin.mjs",
"dsh-plugin-assets"
],
"scripts": {
"prepack": "tsc --noEmit && tsdown src/plugin.ts src/mcp-server.ts --no-config --tsconfig tsconfig.json --out-dir lib --platform node --target es2024 --clean && dsh-plugin-prepare"
},
"dsh": {
"skills": [
"../skills"
],
"mcpServers": "./.mcp.json",
"entry": "./lib/plugin.mjs"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.29.0"
},
"devDependencies": {
"@deepseek-ai/dsh-repository-plugin": "0.0.1",
"cordis": "4.0.0-rc.7",
"tsdown": "0.22.2",
"typescript": "6.0.3"
}
}

View File

@@ -1,19 +0,0 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
// The repository root's linter cannot resolve this independently installed
// Git-package dependency; the package's prepack tsc validates the SDK types.
/* oxlint-disable typescript/no-unsafe-assignment, typescript/no-unsafe-call, typescript/no-unsafe-member-access */
const server = new McpServer({
name: 'github-repository-plugin-e2e',
version: '0.0.0',
})
server.registerTool('proof', {
description: 'Proves that an MCP server compiled from the exact GitHub repository package is active.',
inputSchema: {},
}, async () => ({
content: [{ type: 'text', text: 'MCP_FROM_GITHUB_REPOSITORY' }],
}))
await server.connect(new StdioServerTransport())

View File

@@ -1,59 +0,0 @@
import type { Context } from 'cordis'
const PROOF_TOOL_NAME = 'mcp__github_repository__proof'
interface TextBlock {
readonly type: 'text'
readonly text: string
}
interface ToolExecution {
readonly name: string
}
interface ToolResult {
readonly isError: boolean
readonly content: readonly TextBlock[]
}
type PostDecision =
| { readonly kind: 'accept'; readonly content?: readonly TextBlock[]; readonly value?: unknown; readonly additionalContexts?: readonly unknown[] }
| { readonly kind: 'block'; readonly feedback: readonly TextBlock[] }
type PostListener = (
execution: ToolExecution,
result: ToolResult,
next: () => Promise<PostDecision>,
) => Promise<PostDecision>
type DshContext = Context & {
on(event: 'tools/post-execute', listener: PostListener): () => void
}
/** Cordis plugin name used by the repository acceptance fixture. */
export const name = 'github-repository-typescript-proof'
/** DSH tool registry required by the post-execute contribution. */
export const inject = ['tools']
/**
* Append a marker after the repository MCP proof tool succeeds.
* @param ctx - trusted DSH Cordis context supplied to the repository package.
*/
export function apply(ctx: Context): void {
const dsh = ctx as DshContext
dsh.on('tools/post-execute', async (execution, result, next): Promise<PostDecision> => {
const decision = await next()
if (execution.name !== PROOF_TOOL_NAME || result.isError || decision.kind !== 'accept' || Object.hasOwn(decision, 'value')) {
return decision
}
return {
kind: 'accept',
content: [
...(decision.content ?? result.content),
{ type: 'text', text: 'TS_PLUGIN_FROM_GITHUB_REPOSITORY' },
],
...decision.additionalContexts === undefined ? {} : { additionalContexts: decision.additionalContexts },
}
})
}

View File

@@ -1,13 +0,0 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": [
"src/**/*.ts"
]
}

View File

@@ -1,6 +0,0 @@
---
name: github-source-proof
description: Proves that dsh installed a private repository Plugin from an exact GitHub source.
---
This skill exists only in the GitHub repository source fixture.

View File

@@ -1,256 +0,0 @@
import { createHash } from 'node:crypto'
import { cpSync, existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
import { createServer } from 'node:http'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { startMockLlmServer } from '@deepseek-ai/dsh-llm-mock-server'
import { execa } from 'execa'
import { describe, expect, it } from 'vitest'
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
const repositoryPluginPackage = join(repoRoot, 'packages/self-modification/repository-plugin')
const releasePackageNames = new Set(globSync([
'vendor/*/package.json',
'packages/*/*/package.json',
'apps/*/package.json',
], { cwd: repoRoot }).map((filename) => {
const manifest = JSON.parse(readFileSync(join(repoRoot, filename), 'utf8')) as Record<string, unknown>
if (typeof manifest.name !== 'string') throw new Error(`workspace package name is missing: ${filename}`)
return manifest.name
}))
const source = process.env.DSH_GITHUB_REPOSITORY_PLUGIN_SOURCE
const required = process.env.DSH_REQUIRE_GITHUB_REPOSITORY_PLUGIN_E2E === '1'
const enabled = required || source !== undefined
interface PublishedPackageRegistry {
url: string
requests: string[]
close(): Promise<void>
}
function publishedManifest(): Record<string, unknown> {
const manifest = JSON.parse(readFileSync(join(repositoryPluginPackage, 'package.json'), 'utf8')) as Record<string, unknown>
const version = manifest.version
if (typeof version !== 'string') throw new Error('repository Plugin package version is missing')
Reflect.deleteProperty(manifest, 'private')
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
const dependencies = manifest[field]
if (typeof dependencies !== 'object' || dependencies === null || Array.isArray(dependencies)) continue
const entries = dependencies as Record<string, unknown>
for (const name of Object.keys(entries)) {
if (releasePackageNames.has(name)) {
entries[name] = version
}
}
}
return manifest
}
async function startPublishedPackageRegistry(root: string): Promise<PublishedPackageRegistry> {
const staging = join(root, 'published-repository-plugin')
const artifacts = join(root, 'npm-registry-artifacts')
mkdirSync(staging)
mkdirSync(artifacts)
cpSync(join(repositoryPluginPackage, 'lib'), join(staging, 'lib'), { recursive: true })
for (const filename of ['README.md', 'README.zh.md', 'README.i18n.yaml']) {
cpSync(join(repositoryPluginPackage, filename), join(staging, filename))
}
cpSync(join(repoRoot, 'LICENSE'), join(staging, 'LICENSE'))
const manifest = publishedManifest()
writeFileSync(join(staging, 'package.json'), `${JSON.stringify(manifest, undefined, 2)}\n`)
const packed = await execa('pnpm', ['pack', '--pack-destination', artifacts], {
cwd: staging,
reject: false,
})
if (packed.exitCode !== 0) {
throw new Error(`failed to pack the simulated published prepare package:\n${packed.stderr}\n${packed.stdout}`)
}
const tarballs = readdirSync(artifacts).filter(filename => filename.endsWith('.tgz'))
if (tarballs.length !== 1) throw new Error(`expected one simulated published tarball, found ${tarballs.length}`)
const tarball = readFileSync(join(artifacts, tarballs[0]!))
const name = manifest.name as string
const version = manifest.version as string
const requests: string[] = []
let registryUrl = ''
const server = createServer((request, response) => {
const path = decodeURIComponent(new URL(request.url ?? '/', registryUrl).pathname)
requests.push(`${request.method ?? 'GET'} ${path}`)
if (path === `/${name}`) {
const metadata = {
name,
'dist-tags': { latest: version },
versions: {
[version]: {
...manifest,
dist: {
tarball: `${registryUrl}${name}/-/${name.split('/').at(-1)}-${version}.tgz`,
shasum: createHash('sha1').update(tarball).digest('hex'),
integrity: `sha512-${createHash('sha512').update(tarball).digest('base64')}`,
},
},
},
}
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify(metadata))
return
}
if (path === `/${name}/-/${name.split('/').at(-1)}-${version}.tgz`) {
response.writeHead(200, {
'content-type': 'application/octet-stream',
'content-length': String(tarball.length),
})
response.end(tarball)
return
}
response.writeHead(404, { 'content-type': 'application/json' })
response.end(JSON.stringify({ error: 'not found' }))
})
await new Promise<void>((resolve, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', resolve)
})
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('simulated npm registry did not expose a TCP address')
registryUrl = `http://127.0.0.1:${address.port}/`
return {
url: registryUrl,
requests,
close: () => new Promise<void>((resolve, reject) => {
server.close((error) => { if (error === undefined) resolve(); else reject(error) })
}),
}
}
describe.skipIf(!enabled)('dsh run GitHub repository Plugin installation', () => {
it('installs the published prepare dependency, then builds and runs skill, MCP, and TypeScript Plugin contributions from a private exact GitHub source', async () => {
expect(existsSync(dshBin), 'the repository Plugin acceptance must run the built dsh entry').toBe(true)
expect(source, 'DSH_GITHUB_REPOSITORY_PLUGIN_SOURCE is required by this CI lane').toMatch(
/^github:[^/\s#&]+\/[^/\s#&]+#[0-9a-f]{40}&path:\/.*\/\.dsh-plugin$/u,
)
const apiKey = 'github-repository-plugin-e2e-key'
const server = await startMockLlmServer({
sequence: ['tool_call_success', 'success'],
apiKey,
toolName: 'mcp__github_repository__proof',
toolArguments: '{}',
successText: 'trusted GitHub repository package reached dsh run',
})
const home = mkdtempSync(join(tmpdir(), 'dsh-github-repository-plugin-'))
const registry = await startPublishedPackageRegistry(home)
const npmrc = join(home, 'npmrc')
writeFileSync(npmrc, `@deepseek-ai:registry=${registry.url}\n`)
const hostBin = join(home, 'host-bin')
mkdirSync(hostBin)
writeFileSync(join(hostBin, 'dsh-plugin-prepare'), [
'#!/bin/sh',
'echo "host PATH supplied dsh-plugin-prepare instead of the declared npm dependency" >&2',
'exit 91',
'',
].join('\n'), { mode: 0o700 })
const patch = join(home, 'github-repository-plugin.cordis.patch.yml')
writeFileSync(patch, [
'- id: repository-plugins',
' config:',
' repositories:',
` - ${JSON.stringify(source)}`,
'- id: session-title-llm',
' disabled: true',
'',
].join('\n'))
try {
const result = await execa(process.execPath, [
dshBin,
'run',
'--patch',
patch,
'prove the private GitHub repository Plugin is active',
], {
cwd: repoRoot,
input: '',
timeout: 180_000,
killSignal: 'SIGKILL',
reject: false,
env: {
...process.env,
DSH_HOME: home,
DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: apiKey,
DEEPSEEK_BASE_URL: server.baseURL,
NPM_CONFIG_USERCONFIG: npmrc,
// A warm runner cache could satisfy the exact tarball without
// contacting this test's registry, which would stop proving the
// unpublished package was installed through the simulated release.
PNPM_CONFIG_CACHE_DIR: join(home, 'pnpm-cache'),
PNPM_CONFIG_STORE_DIR: join(home, 'pnpm-store'),
PATH: process.env.PATH === undefined ? hostBin : `${hostBin}${delimiter}${process.env.PATH}`,
},
})
if (result.timedOut) {
throw new Error(`dsh GitHub repository Plugin run did not exit within 180s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
}
expect(result.exitCode, `${result.stderr}\nstdout:\n${result.stdout}`).toBe(0)
expect(result.stdout).toBe('trusted GitHub repository package reached dsh run')
expect(server.requests).toHaveLength(2)
const runtimeDiagnostic = `${result.stderr}\nstdout:\n${result.stdout}`
expect(registry.requests, runtimeDiagnostic).toContain('GET /@deepseek-ai/dsh-repository-plugin')
expect(registry.requests, runtimeDiagnostic).toContain('GET /@deepseek-ai/dsh-repository-plugin/-/dsh-repository-plugin-0.0.1.tgz')
const firstRequest = JSON.stringify(server.requests[0]!.body)
const secondRequest = JSON.stringify(server.requests[1]!.body)
expect(firstRequest, runtimeDiagnostic).toContain(
'Proves that dsh installed a private repository Plugin from an exact GitHub source.',
)
expect(firstRequest, runtimeDiagnostic).toContain('mcp__github_repository__proof')
expect(firstRequest, runtimeDiagnostic).toContain('Proves that an MCP server compiled from the exact GitHub repository package is active.')
expect(secondRequest, runtimeDiagnostic).toContain('MCP_FROM_GITHUB_REPOSITORY')
expect(secondRequest, runtimeDiagnostic).toContain('TS_PLUGIN_FROM_GITHUB_REPOSITORY')
const cacheRoot = join(home, 'cache', 'repository-plugins')
const generations = readdirSync(cacheRoot, { withFileTypes: true }).filter(entry => entry.isDirectory())
expect(generations).toHaveLength(1)
const installed = join(cacheRoot, generations[0]!.name, 'node_modules', 'repository')
const manifest = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8')) as Record<string, unknown>
expect(manifest).toMatchObject({
name: 'dsh-github-repository-plugin-e2e-fixture',
private: true,
scripts: {
prepack: 'tsc --noEmit && tsdown src/plugin.ts src/mcp-server.ts --no-config --tsconfig tsconfig.json --out-dir lib --platform node --target es2024 --clean && dsh-plugin-prepare',
},
dsh: {
skills: ['../skills'],
mcpServers: './.mcp.json',
entry: './lib/plugin.mjs',
},
dependencies: {
'@modelcontextprotocol/sdk': '1.29.0',
},
devDependencies: {
'@deepseek-ai/dsh-repository-plugin': '0.0.1',
cordis: '4.0.0-rc.7',
tsdown: '0.22.2',
typescript: '6.0.3',
},
})
expect(readFileSync(join(installed, 'dsh-plugin-assets/skills/0/github-source-proof/SKILL.md'), 'utf8'))
.toContain('This skill exists only in the GitHub repository source fixture.')
expect(readFileSync(join(installed, 'dsh-plugin-assets/.mcp.json'), 'utf8')).toContain('lib/mcp-server.mjs')
expect(readFileSync(join(installed, 'lib/plugin.mjs'), 'utf8')).toContain('TS_PLUGIN_FROM_GITHUB_REPOSITORY')
expect(readFileSync(join(installed, 'lib/mcp-server.mjs'), 'utf8')).toContain('MCP_FROM_GITHUB_REPOSITORY')
expect(existsSync(join(installed, 'src'))).toBe(false)
const installedRequire = createRequire(join(installed, 'lib/mcp-server.mjs'))
expect(existsSync(installedRequire.resolve('@modelcontextprotocol/sdk/server/mcp.js'))).toBe(true)
const wrapper = readFileSync(join(installed, 'dsh-plugin.mjs'), 'utf8')
expect(wrapper).toContain('dsh-repository-plugin')
expect(wrapper).toContain('await import(manifest.entry)')
expect(wrapper).toContain('"entry":"./lib/plugin.mjs"')
} finally {
await server.close()
await registry.close()
rmSync(home, { recursive: true, force: true })
}
}, 190_000)
})

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: 471680f92dc44f3dd4e98ba9e946525ec79f25b0
config-catalog.zh.md: bf78766799b02a1f6f21f935723abace108bc306
config-catalog.md: 10078dea5221cfe76b1c028ef83216f5a4575940
config-catalog.zh.md: 38ed9f94bee9b0c11f5ea4dad4ba48532cce6450

View File

@@ -1109,7 +1109,7 @@ export interface StreamableHttpConfig {
}
```
Source: [`packages/mcp/mcp-client/src/index.ts:100`](../packages/mcp/mcp-client/src/index.ts)
Source: [`packages/mcp/mcp-client/src/index.ts:94`](../packages/mcp/mcp-client/src/index.ts)
## `@deepseek-ai/dsh-permission`
@@ -1306,22 +1306,6 @@ export interface Config {
Source: [`packages/guard/repeat-tool-guard/src/index.ts:28`](../packages/guard/repeat-tool-guard/src/index.ts)
## `@deepseek-ai/dsh-repository-plugin`
Requires: `loader`
```ts config-catalog
/** Repository Plugin runtime and source-list configuration. */
export interface Config {
/** GitHub repository sources with explicit refs and optional `.dsh-plugin` subpaths. */
repositories?: string[]
/** Persistent generation cache; defaults to `$DSH_HOME/cache/repository-plugins`. */
cacheDir?: string
}
```
Source: [`packages/self-modification/repository-plugin/src/index.ts:44`](../packages/self-modification/repository-plugin/src/index.ts)
## `@deepseek-ai/dsh-sandbox-local`
```ts config-catalog

View File

@@ -1111,7 +1111,7 @@ export interface StreamableHttpConfig {
}
```
来源:[`packages/mcp/mcp-client/src/index.ts:100`](../packages/mcp/mcp-client/src/index.ts)
来源:[`packages/mcp/mcp-client/src/index.ts:94`](../packages/mcp/mcp-client/src/index.ts)
## `@deepseek-ai/dsh-permission`
@@ -1308,22 +1308,6 @@ export interface Config {
来源:[`packages/guard/repeat-tool-guard/src/index.ts:28`](../packages/guard/repeat-tool-guard/src/index.ts)
## `@deepseek-ai/dsh-repository-plugin`
需要:`loader`
```ts config-catalog
/** Repository Plugin runtime and source-list configuration. */
export interface Config {
/** GitHub repository sources with explicit refs and optional `.dsh-plugin` subpaths. */
repositories?: string[]
/** Persistent generation cache; defaults to `$DSH_HOME/cache/repository-plugins`. */
cacheDir?: string
}
```
来源:[`packages/self-modification/repository-plugin/src/index.ts:44`](../packages/self-modification/repository-plugin/src/index.ts)
## `@deepseek-ai/dsh-sandbox-local`
```ts config-catalog

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/module-graph.md
module-graph.md: 5b1a0ecb4d6c835d62757971904cd3fd040a1ea2
module-graph.zh.md: 6ba33333c97a10cfeb5f5404ee210db4c54c9df6
module-graph.md: 2b1f8dd9d41ab5ad34a4787ffa54c6b7d13144af
module-graph.zh.md: 192943312b672c1ae10d32182e9ccd7456eac90b

View File

@@ -253,7 +253,6 @@ flowchart TD
pkg_telemetry["telemetry"]
end
subgraph group_self_modification["packages/self-modification"]
pkg_repository_plugin["repository-plugin"]
pkg_tool_cordis["tool-cordis"]
end
subgraph group_session["packages/session"]
@@ -1070,10 +1069,6 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
pkg_repository_plugin --> pkg_invariants
pkg_repository_plugin --> pkg_mcp_client
pkg_repository_plugin --> pkg_paths
pkg_repository_plugin --> pkg_skill_local
pkg_tool_ralph --> pkg_agent
pkg_tool_ralph --> pkg_invariants
pkg_tool_ralph --> pkg_llm
@@ -1431,7 +1426,6 @@ flowchart TD
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |

View File

@@ -255,7 +255,6 @@ flowchart TD
pkg_telemetry["telemetry"]
end
subgraph group_self_modification["packages/self-modification"]
pkg_repository_plugin["repository-plugin"]
pkg_tool_cordis["tool-cordis"]
end
subgraph group_session["packages/session"]
@@ -1072,10 +1071,6 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
pkg_repository_plugin --> pkg_invariants
pkg_repository_plugin --> pkg_mcp_client
pkg_repository_plugin --> pkg_paths
pkg_repository_plugin --> pkg_skill_local
pkg_tool_ralph --> pkg_agent
pkg_tool_ralph --> pkg_invariants
pkg_tool_ralph --> pkg_llm
@@ -1433,7 +1428,6 @@ flowchart TD
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/tool-catalog.md
tool-catalog.md: dbab9ce2f389dbfe40e7d753ced995a8a384be17
tool-catalog.zh.md: e99f8bc78923e616265427c1e0361c832cc0930f
tool-catalog.md: f61b6daeb7209d0fa81606bda7718fd42c7f22dc
tool-catalog.zh.md: cbecdba084fa64ec78913706221a1558d206cde8

View File

@@ -284,7 +284,7 @@ Source: [`packages/self-modification/tool-cordis/src/index.ts`](../packages/self
### `cordis_mount`
Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.
Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.
```json
{

View File

@@ -286,7 +286,7 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
### `cordis_mount`
在当前 DSH 进程中挂载临时 Cordis Plugin。它创建的是内存中的运行时 Plugin而不是已安装或已配置的 Plugin。该插件会在后续轮次中保持活动直到执行 cordis_unmount、工具集卸载或 DSH 重启。它不会创建文件、安装包、修改 cordis.yml 或个人/项目配置、在重启后保留,也不会自动转为永久插件。若要保留,请让 Agent 通过常规开发工作流实现普通的本地、项目或仓库 Plugin。它可能影响同一进程中的其他会话;沙箱不是安全边界,注入的服务会访问真实运行时。`code` 会立即作为异步 JavaScript 函数的函数体在隔离沙箱中运行,并且**必须** `return` 一个插件。支持两种形式:函数形式 `return (ctx) => { … }`,它不声明 inject因此可以注册工具、监听事件和提供服务但访问**任何**服务(例如 ctx.bash都会抛出异常仅在不需要服务时使用。对象形式 `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }`它声明依赖Cordis 只在服务存在后激活插件;**优先使用**这种形式。你只能访问 inject 中列出的服务:即使未声明的服务存在,访问它也会抛出异常,因为如果提供方被卸载,未声明的依赖将无法清理。代码调用服务**之前**,请读取 cordis_inspect 的 what:"api";它会列出方法签名以及参数/返回值的类型形状,不要猜测字段类型,例如 bash 运行的 stdout 是对象而非字符串。在 `apply` 内,请使用标准 Cordis API通过 `ctx.on(event, listener)` 观察事件(见 cordis_inspect 的 what:"events"),或调用 `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` 为自己提供新工具;该工具会在你的**下一步骤**可调用。工具参数:每个键**就是**一个属性,即 { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? };每个直接 DSL 对象都声明 additionalProperties: true|false而 oneOf: [schema, schema, ...] 会取代 type表示恰好匹配一个成员的联合。也接受原始 JSON Schema { type: 'object', properties, required?: […] } 包装层,其中对象默认开放。工具的 `execute` **必须**返回 `output.schema` 声明的无损 JSON 值;`output.render(args, value)` 单独返回 Native模型内容块。临时 Plugin 可以**组合**:一个 Plugin 可以通过 `ctx.provide('name', value)` 提供服务,另一个则可声明 `inject: ['name']` 来消费它;消费方会在提供方出现前保持等待,提供方卸载后重新回到等待状态。在 `apply` 中注册的一切都会由 cordis_unmount 自动清理。沙箱全局对象:`console`(带 `[cordis:<id>]` 标签,写入 harness 终端)、`harness.defineTool``harness.registerTool``btoa``atob``TextEncoder``TextDecoder`。Node API 已**禁用**:文件系统/网络/定时工作必须通过 Cordis 服务完成,绝不能使用 Node 内置能力;`require``setTimeout``setInterval``fetch` 会抛出重定向错误,`process``Buffer` 未定义。应改用 inject: ['fs'] + ctx.fs 处理文件、inject: ['web'] + ctx.web 处理 HTTP、inject: ['bash'] + ctx.bash 处理进程、inject: ['timer'] + ctx.setTimeout/ctx.setInterval 处理定时(这些是 fiber effect卸载时自动清理cordis_inspect 的 what:"api" 会展示**当前**运行时提供的能力。请编写**纯** JavaScript不要使用 TypeScript不得使用 `as` 或类型注解)。注意事项:(1) waterfall瀑布式事件事件例如 tools/pre-execute会向监听器传入最后一个 `next` 回调,该回调**必须**被调用;不调用 `next()` 就返回会**短路**此次调用。除非你有意拦截,否则请优先使用普通通知事件。(2) 切勿等待只能在当前轮次之后解析的内容;你的代码运行在该轮次的工具调用**内部**,否则会死锁。(3) 你的 `ctx` 是受限门面可以注册工具、观察事件、提供消费服务和使用定时器但不会提供框架内部能力ctx.root、ctx.fiber、ctx.extend、ctx.plugin 等)。不过,它并非安全边界:你注入的服务(例如 ctx.bash会访问真实运行时。
在当前 DSH 进程中挂载临时 Cordis Plugin。它创建的是内存中的运行时 Plugin而不是已安装或已配置的 Plugin。该插件会在后续轮次中保持活动直到执行 cordis_unmount、工具集卸载或 DSH 重启。它不会创建文件、安装包、修改 cordis.yml 或个人/项目配置、在重启后保留,也不会自动转为永久插件。若要保留,请让 Agent 通过常规开发工作流实现 SDK Plugin 或可安装的 profile bundle。它可能影响同一进程中的其他会话;沙箱不是安全边界,注入的服务会访问真实运行时。`code` 会立即作为异步 JavaScript 函数的函数体在隔离沙箱中运行,并且**必须** `return` 一个插件。支持两种形式:函数形式 `return (ctx) => { … }`,它不声明 inject因此可以注册工具、监听事件和提供服务但访问**任何**服务(例如 ctx.bash都会抛出异常仅在不需要服务时使用。对象形式 `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }`它声明依赖Cordis 只在服务存在后激活插件;**优先使用**这种形式。你只能访问 inject 中列出的服务:即使未声明的服务存在,访问它也会抛出异常,因为如果提供方被卸载,未声明的依赖将无法清理。代码调用服务**之前**,请读取 cordis_inspect 的 what:"api";它会列出方法签名以及参数/返回值的类型形状,不要猜测字段类型,例如 bash 运行的 stdout 是对象而非字符串。在 `apply` 内,请使用标准 Cordis API通过 `ctx.on(event, listener)` 观察事件(见 cordis_inspect 的 what:"events"),或调用 `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` 为自己提供新工具;该工具会在你的**下一步骤**可调用。工具参数:每个键**就是**一个属性,即 { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? };每个直接 DSL 对象都声明 additionalProperties: true|false而 oneOf: [schema, schema, ...] 会取代 type表示恰好匹配一个成员的联合。也接受原始 JSON Schema { type: 'object', properties, required?: […] } 包装层,其中对象默认开放。工具的 `execute` **必须**返回 `output.schema` 声明的无损 JSON 值;`output.render(args, value)` 单独返回 Native模型内容块。临时 Plugin 可以**组合**:一个 Plugin 可以通过 `ctx.provide('name', value)` 提供服务,另一个则可声明 `inject: ['name']` 来消费它;消费方会在提供方出现前保持等待,提供方卸载后重新回到等待状态。在 `apply` 中注册的一切都会由 cordis_unmount 自动清理。沙箱全局对象:`console`(带 `[cordis:<id>]` 标签,写入 harness 终端)、`harness.defineTool``harness.registerTool``btoa``atob``TextEncoder``TextDecoder`。Node API 已**禁用**:文件系统/网络/定时工作必须通过 Cordis 服务完成,绝不能使用 Node 内置能力;`require``setTimeout``setInterval``fetch` 会抛出重定向错误,`process``Buffer` 未定义。应改用 inject: ['fs'] + ctx.fs 处理文件、inject: ['web'] + ctx.web 处理 HTTP、inject: ['bash'] + ctx.bash 处理进程、inject: ['timer'] + ctx.setTimeout/ctx.setInterval 处理定时(这些是 fiber effect卸载时自动清理cordis_inspect 的 what:"api" 会展示**当前**运行时提供的能力。请编写**纯** JavaScript不要使用 TypeScript不得使用 `as` 或类型注解)。注意事项:(1) waterfall瀑布式事件事件例如 tools/pre-execute会向监听器传入最后一个 `next` 回调,该回调**必须**被调用;不调用 `next()` 就返回会**短路**此次调用。除非你有意拦截,否则请优先使用普通通知事件。(2) 切勿等待只能在当前轮次之后解析的内容;你的代码运行在该轮次的工具调用**内部**,否则会死锁。(3) 你的 `ctx` 是受限门面可以注册工具、观察事件、提供消费服务和使用定时器但不会提供框架内部能力ctx.root、ctx.fiber、ctx.extend、ctx.plugin 等)。不过,它并非安全边界:你注入的服务(例如 ctx.bash会访问真实运行时。
```json
{

View File

@@ -1,13 +1,13 @@
{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
{"type":"approval/policy","seq":0,"time":1786357538290,"data":{"policy":"never","source":"delegation"}}
{"type":"agent/inbox/spliced","seq":1,"time":1786357538290,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"ebe0cfa0-a909-47e0-8294-28ad84a8fe77"}]}}
{"type":"turn/start","seq":2,"time":1786357538290,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":3,"time":1786357538290,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"subagent/descriptor","seq":4,"time":1786357538308,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}}
{"type":"step/start","seq":5,"time":1786357538310,"data":{"turn":1,"step":1}}
{"type":"approval/policy","seq":0,"time":1786370620371,"data":{"policy":"never","source":"delegation"}}
{"type":"agent/inbox/spliced","seq":1,"time":1786370620371,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"ebe0cfa0-a909-47e0-8294-28ad84a8fe77"}]}}
{"type":"turn/start","seq":2,"time":1786370620372,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":3,"time":1786370620372,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"subagent/descriptor","seq":4,"time":1786370620388,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}}
{"type":"step/start","seq":5,"time":1786370620390,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730458555,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"ebe0cfa0-a909-47e0-8294-28ad84a8fe77"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357538310,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"baed758f-a123-4c3d-8587-a5b7d854f71f"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357538310,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"user/message","seq":7,"time":1786370620390,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"a66e9b7d-dd55-4261-9931-4cf11dab818f"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786370620390,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730458555,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730458555,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"assistant/chunk","seq":11,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}

View File

@@ -1,13 +1,13 @@
{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
{"type":"approval/policy","seq":0,"time":1786357538450,"data":{"policy":"never","source":"delegation"}}
{"type":"agent/inbox/spliced","seq":1,"time":1786357538450,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ac2cc54-9bce-4cfa-a569-a64f51bc30a7"}]}}
{"type":"turn/start","seq":2,"time":1786357538450,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":3,"time":1786357538450,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"subagent/descriptor","seq":4,"time":1786357538469,"data":{"version":2,"mode":"one-shot","provider":"spawn"}}
{"type":"step/start","seq":5,"time":1786357538470,"data":{"turn":1,"step":1}}
{"type":"approval/policy","seq":0,"time":1786370620528,"data":{"policy":"never","source":"delegation"}}
{"type":"agent/inbox/spliced","seq":1,"time":1786370620529,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ac2cc54-9bce-4cfa-a569-a64f51bc30a7"}]}}
{"type":"turn/start","seq":2,"time":1786370620529,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":3,"time":1786370620529,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"subagent/descriptor","seq":4,"time":1786370620551,"data":{"version":2,"mode":"one-shot","provider":"spawn"}}
{"type":"step/start","seq":5,"time":1786370620553,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730458703,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ac2cc54-9bce-4cfa-a569-a64f51bc30a7"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357538471,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"01f64214-c832-47ef-8e90-052047edc27d"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357538471,"data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"user/message","seq":7,"time":1786370620553,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"a72ce7b4-53f3-4525-ae16-4f7cbaf196b1"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786370620553,"data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730458703,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730458703,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"assistant/chunk","seq":11,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}

View File

@@ -4,7 +4,7 @@
{"type":"agent/inbox/spliced","seq":2,"time":1785821417919,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1783957884486,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1785498801761,"data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: try a no-op temporary Cordis Plugin named snapshot-marker; use run_code to inspect the live temporary Plugins through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; stop dyn-1; then reply with exactly ADVANCED_ACP_OK."}],"source":{"kind":"user"},"role":"user","id":"6e45782a-31be-4ba7-8c4a-7411a2027e36"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1785730458430,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"06416873-c855-452d-8996-ea5cf45223d1"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1785730458430,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"2536e0b7-481b-4b7c-b55a-8af582f23f6a"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730458430,"data":{"title":"Run this advanced flow exactly","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785498801765,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730458431,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}

View File

@@ -60,7 +60,7 @@ interface ToolArgsMap {
/** Exact service key or event name whose original JSDoc to include; valid only with what:"api" or what:"events". */
name?: string;
} & Record<string, JsonValue>;
/** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */
/** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */
cordis_mount: {
/** JavaScript body returning a temporary Plugin; evaluated now and saved nowhere. */
code: string;

View File

@@ -72,7 +72,7 @@
},
{
"name": "cordis_mount",
"description": "Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.",
"description": "Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.",
"parameters": {
"type": "object",
"properties": {

View File

@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357533600,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth one"}}
{"type":"step/start","seq":5,"time":1786357533602,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730456014,"data":{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"},"role":"user","id":"a8129357-1bde-4cbd-90b4-6b8ad51d52e1"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357533602,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"82a11d62-da49-4ad3-a243-789ea3cd7c08"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357533602,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"2c09c395-d7f0-44cf-b7be-5801a811e80c"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357533602,"data":{"title":"Call subagent once. Ask that","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730456014,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730456014,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}

View File

@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357533628,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth two"}}
{"type":"step/start","seq":5,"time":1786357533630,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730456041,"data":{"content":[{"type":"text","text":"Attempt one subagent call beyond the configured cap, then report the rejection."}],"source":{"kind":"user"},"role":"user","id":"d4dc5a16-e542-4dd9-8e82-e6b7829cfc4b"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357533630,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"d5488efe-eea2-4019-8fcb-7e6e49077d8a"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357533630,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"d71d9613-e800-4e29-b617-a0f327dcc61c"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357533630,"data":{"title":"Attempt one subagent call beyond","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730456041,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730456042,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}

View File

@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357524752,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Reply ALPHA only"}}
{"type":"step/start","seq":5,"time":1786357524755,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730450187,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"73ce401a-faaf-408a-879e-7485380d537d"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357524755,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"56ad93fa-0cc0-4ccb-a1b4-258f4801c681"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357524755,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"854dbbdb-e52a-4006-b958-6da541481af6"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357524755,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730450187,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730450188,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}

View File

@@ -27,7 +27,7 @@
{"type":"subagent/descriptor","seq":41,"time":1786357524800,"data":{"version":2,"mode":"one-shot","provider":"fork","label":"Recall project codeword"}}
{"type":"step/start","seq":42,"time":1786357524803,"data":{"turn":2,"step":1}}
{"type":"user/message","seq":43,"time":1786357524803,"data":{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"86e9f144-764f-460d-b72b-262cffe43d77"},"surfaceOp":"append"}
{"type":"user/message","seq":44,"time":1786358036899,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"6ea5a774-b0da-47ff-84b7-226a4a207bbf"},"surfaceOp":"append"}
{"type":"user/message","seq":44,"time":1786358036899,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"e5e2f36a-eb19-47c7-8ddc-d511ee152874"},"surfaceOp":"append"}
{"type":"request/header","seq":45,"time":1786358036900,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"resume"}}
{"type":"assistant/chunk","seq":46,"time":1783352148077,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
{"type":"reasoning-chunks","seq0":47,"time0":1783352148077,"data":{"turn":2,"step":1,"index":0,"dt":[0,0,29,0,0,0,35,0,0,0,0,26,29,31,0,30,0,0,27,1,27,0,1,0,0,31,1,0,0,1790157964],"texts":["The"," user"," is"," asking"," me"," to"," recall"," the"," project"," cod","ew","ord"," that"," was"," mentioned"," earlier"," in"," the"," conversation","."," I"," was"," told"," to"," remember"," it",":"," SA","FF","RON","."]}}

View File

@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357521754,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return ALPHA only"}}
{"type":"step/start","seq":5,"time":1786357521756,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730447828,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"a287f842-f6f2-4a17-ab4c-820e41f498d5"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357521756,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"f654b5a4-b4c0-4443-8eab-d84624d804f1"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357521756,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"07c1eb3c-4f30-4c09-bdb3-3126eccfb1ce"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357521756,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730447828,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730447828,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}

View File

@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357521799,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return BETA only"}}
{"type":"step/start","seq":5,"time":1786357521801,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730447881,"data":{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"53f6419d-8ddc-4eee-8803-5b68411336f9"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357521802,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"05843da4-4a5f-46fc-a00f-5257b2bd271d"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357521802,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"aef86ec0-c961-4beb-b264-1a225a80b3b9"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357521802,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730447881,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730447881,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}

View File

@@ -1,9 +1,6 @@
- id: cli-mock-llm
name: './cli-mock-llm.ts'
- id: repository-plugin-fixture
name: './repository-plugin/load.mjs'
- id: base
name: '@cordisjs/plugin-include'
config:

View File

@@ -1,6 +0,0 @@
---
name: repository-fixture
description: Repository fixture skill.
---
Static instructions from a prepared repository plugin.

View File

@@ -1,19 +0,0 @@
// Generated by dsh-plugin-prepare. Do not edit.
const manifest = {"name":"headless-repository-fixture","skills":["dsh-plugin-assets/skills/0"]}
// Value mirror: Cordis const enum FiberState.ACTIVE; keep aligned with dsh-repository-plugin source.ts.
const FIBER_ACTIVE = 2
export const name = "headless-repository-fixture"
export const inject = ["loader","skills"]
async function mount(ctx, plugin, label, config) {
const fiber = ctx.plugin(plugin, config)
await fiber
if (fiber.state !== FIBER_ACTIVE) {
const missing = Object.keys(fiber.inject).filter(service => fiber.ctx.get(service) === undefined)
throw new Error(`${label} did not activate (waiting for services: ${missing.join(', ') || 'unknown'})`)
}
}
export async function apply(ctx) {
const runtime = ctx.loader.builtins["dsh-repository-plugin"]
if (runtime === undefined) throw new Error("missing Cordis builtin dsh-repository-plugin")
await mount(ctx, runtime, 'repository Plugin runtime', { baseUrl: import.meta.url, manifest })
}

View File

@@ -1,13 +0,0 @@
/**
* Keyless fixture owner that mounts the runtime before its prepared wrapper.
* Cordis starts sibling Loader entries concurrently, so row order is not a dependency edge.
*/
import * as RepositoryPlugin from '@deepseek-ai/dsh-repository-plugin'
import * as PreparedPlugin from './dsh-plugin.mjs'
export const name = 'headless-repository-fixture-loader'
export async function apply(ctx) {
await ctx.plugin(RepositoryPlugin)
await ctx.plugin(PreparedPlugin)
}

View File

@@ -1,17 +1,10 @@
import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { readFile, readdir } from 'node:fs/promises'
import { zstdDecompress } from 'node:zlib'
import { promisify } from 'node:util'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import {
PREPARED_ENTRY_FILENAME,
REPOSITORY_PLUGIN_PREPARE_COMMAND,
REPOSITORY_PLUGIN_PACKAGE_NAME,
prepareDshPlugin,
} from '@deepseek-ai/dsh-repository-plugin'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
@@ -44,16 +37,6 @@ describe('headless-agent keyless smoke', () => {
const result = lines.at(-1)
expect(stderr).toBe('')
expect(events.some(event => event.type === 'tool/call' && event.data.name === 'bash')).toBe(true)
const catalogMessage = events.find(event => event.type === 'user/message'
&& event.data.source.kind === 'skill-catalog')
const catalog = catalogMessage?.type === 'user/message'
? catalogMessage.data.content.filter(block => block.type === 'text').map(block => block.text).join('\n')
: ''
expect(catalog.split('\n').find(line => line.includes('repository-fixture'))).toMatchInlineSnapshot(
`
"- \`repository-fixture\`: Repository fixture skill."
`,
)
const toolResult = events.find(event => event.type === 'tool/result')
expect(JSON.stringify(toolResult)).toContain('CLI_TOOL_ROUND_TRIP')
expect(result).toMatchObject({
@@ -63,30 +46,4 @@ describe('headless-agent keyless smoke', () => {
expect(String(result?.['output'])).toContain('CLI_TOOL_ROUND_TRIP')
expect(persistedHeader).toMatchObject({ type: 'session' })
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('keeps the checked-in prepared wrapper identical to the generator output for its manifest', async () => {
// The fixture claims "Generated by dsh-plugin-prepare"; this pin makes the
// claim true — a wrapper-template change fails here until the fixture is
// regenerated, so the assembled smoke can never exercise stale generated fields.
const fixture = fileURLToPath(new URL('./fixtures/repository-plugin/', import.meta.url))
const root = await mkdtemp(join(tmpdir(), 'dsh-fixture-drift-'))
try {
const plugin = join(root, '.dsh-plugin')
await mkdir(plugin, { recursive: true })
await cp(join(fixture, 'dsh-plugin-assets/skills/0'), join(root, 'skills'), { recursive: true })
await writeFile(join(plugin, 'package.json'), `${JSON.stringify({
name: 'headless-repository-fixture',
version: '0.0.0',
scripts: { prepack: REPOSITORY_PLUGIN_PREPARE_COMMAND },
devDependencies: { [REPOSITORY_PLUGIN_PACKAGE_NAME]: '0.0.1' },
dsh: { skills: ['../skills'] },
}, undefined, 2)}\n`)
await prepareDshPlugin(plugin)
const generated = await readFile(join(plugin, PREPARED_ENTRY_FILENAME), 'utf8')
const checkedIn = await readFile(join(fixture, PREPARED_ENTRY_FILENAME), 'utf8')
expect(checkedIn).toBe(generated)
} finally {
await rm(root, { recursive: true, force: true })
}
})
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -50,7 +50,6 @@
"@deepseek-ai/dsh-pty-local": "workspace:*",
"@deepseek-ai/dsh-pwsh-local": "workspace:*",
"@deepseek-ai/dsh-repeat-tool-guard": "workspace:*",
"@deepseek-ai/dsh-repository-plugin": "workspace:*",
"@deepseek-ai/dsh-sandbox": "workspace:*",
"@deepseek-ai/dsh-sandbox-local": "workspace:*",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",

View File

@@ -718,20 +718,6 @@
"@deepseek-ai/.+"
]
},
"apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin": {
"entry": [
"src/*.ts"
],
"project": [
"src/**/*.ts"
],
"ignoreDependencies": [
"@deepseek-ai/dsh-repository-plugin"
],
"ignoreBinaries": [
"dsh-plugin-prepare"
]
},
"packages/client/modules": {
"entry": [
"tests/**/*.spec.ts"

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/README.md
README.md: c246534a26cd7297f2ba8885099c8b517a5dc2b0
README.zh.md: 4139874d3ebbf82fad8680a3721a1cf35a553706
README.md: 19d6e5ba7b554f59bd66e213f8a53389761fc735
README.zh.md: 17f58a2922e9019af054b0dccb6c4d9199fd1a9d

View File

@@ -38,7 +38,7 @@ Groups hold `packages/<group>/<pkg>/`; names stay `@deepseek-ai/dsh-<pkg>`. **Gr
| [`preset/`](preset/README.md) | Per-session agent composition from preset `cordis.yml` files | Product — stable surface |
| [`guard/`](guard/README.md) | Loop-hygiene guards: advisory repeat-call reminders + the `tools/execute` deadline enforcer | Product — stable surface |
| [`bundle/`](bundle/README.md) | Installable `dsh --profile` patch layers | Product — stable surface |
| [`self-modification/`](self-modification/README.md) | Agent runtime self-modification: live plugin/service inspection, model-written plugin mount/unmount ([design](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)), restricted repository Plugin loading | Product — stable surface |
| [`self-modification/`](self-modification/README.md) | Agent runtime self-modification: live plugin/service inspection and model-written plugin mount/unmount ([design](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)) | Product — stable surface |
| [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable surface |
| [`session/`](session/README.md) | Durable session data plane: persistence seam + JSONL/SQLite backends, projection seam, log-backed titles, session reporting | Product — stable surface |
| [`session-query/`](session-query/README.md) | Session retrieval family: logical corpus, bounded reads, lineage, event relationships, semantic filtering, and SQLite full-text search | Product — stable surface |

View File

@@ -38,7 +38,7 @@ npm scope 为 `@deepseek-ai/dsh-*`Cordis `Service` 子类和函数插件通
| [`preset/`](preset/README.md) | 由 preset `cordis.yml` 按会话组装 agent | 产品:稳定接口 |
| [`guard/`](guard/README.md) | 循环卫生守卫:建议性重复调用提醒 + `tools/execute` 截止时间强制执行器 | 产品:稳定接口 |
| [`bundle/`](bundle/README.md) | 可安装的 `dsh --profile` 补丁层 | 产品:稳定接口 |
| [`self-modification/`](self-modification/README.md) | agent 运行时自修改:实时插件/服务检查模型所写插件挂载/卸载([设计](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)、受限仓库插件加载 | 产品:稳定接口 |
| [`self-modification/`](self-modification/README.md) | agent 运行时自修改:实时插件/服务检查模型所写插件挂载/卸载([设计](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md) | 产品:稳定接口 |
| [`hooks/`](hooks/README.md) | 钩子桥接 + 共享 Claude CodeCodex 协议格式库 | 产品:稳定接口 |
| [`session/`](session/README.md) | 持久会话数据平面:持久化 seam + JSONL/SQLite 后端、投影 seam、日志支持的标题、会话上报 | 产品:稳定接口 |
| [`session-query/`](session-query/README.md) | 会话检索系列:逻辑语料库、有界读取、血缘、事件关系、语义过滤和 SQLite 全文搜索 | 产品:稳定接口 |

View File

@@ -1,218 +0,0 @@
import { execFile } from 'node:child_process'
import { createHash } from 'node:crypto'
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { promisify } from 'node:util'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { BUNDLED_PNPM_VERSION, RepositoryCache, type RepositoryInstall } from '@cordisjs/plugin-loader/repository'
const execFileAsync = promisify(execFile)
const roots: string[] = []
/** Normalize Git's platform checkout line endings for source-content assertions. */
const lf = (text: string): string => text.replace(/\r\n/g, '\n')
async function temporaryRoot(name: string): Promise<string> {
const root = await mkdtemp(join(tmpdir(), `cordis-${name}-`))
roots.push(root)
return root
}
async function fakePackage(directory: string): Promise<void> {
const target = join(directory, 'node_modules', 'repository')
await mkdir(target, { recursive: true })
await writeFile(join(target, 'package.json'), '{"name":"fixture"}\n')
}
afterEach(async () => {
vi.unstubAllEnvs()
await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true })))
})
describe('RepositoryCache', () => {
it('single-flights and permanently reuses an exact specifier', async () => {
const root = await temporaryRoot('repository-cache')
const calls: string[] = []
const install: RepositoryInstall = async (directory) => {
calls.push(directory)
await fakePackage(directory)
}
const cache = new RepositoryCache(root, { install })
const specifier = 'github:owner/repository#0123456789abcdef'
const [first, concurrent] = await Promise.all([cache.resolve(specifier), cache.resolve(specifier)])
expect(concurrent).toBe(first)
expect(calls).toHaveLength(1)
const reopened = new RepositoryCache(root, { install: async () => { throw new Error('cache miss') } })
expect(await reopened.resolve(specifier)).toBe(first)
expect(JSON.parse(await readFile(join(first, '..', '..', 'package.json'), 'utf8'))).toMatchObject({
packageManager: `pnpm@${BUNDLED_PNPM_VERSION}`,
dependencies: { repository: specifier },
})
const second = await cache.resolve('github:owner/repository#fedcba9876543210')
expect(second).not.toBe(first)
expect(calls).toHaveLength(2)
})
it('accepts the valid winner when independent cache instances race', async () => {
const root = await temporaryRoot('repository-race')
const bothStarted = Promise.withResolvers<undefined>()
let starts = 0
const install: RepositoryInstall = async (directory) => {
await fakePackage(directory)
starts += 1
if (starts === 2) bothStarted.resolve(undefined)
await bothStarted.promise
}
const specifier = 'github:owner/repository#race'
const [first, second] = await Promise.all([
new RepositoryCache(root, { install }).resolve(specifier),
new RepositoryCache(root, { install }).resolve(specifier),
])
expect(second).toBe(first)
expect(starts).toBe(2)
expect(await readdir(root)).toHaveLength(1)
})
it('removes a failed staging tree and permits an exact retry', async () => {
const root = await temporaryRoot('repository-retry')
let attempts = 0
const cache = new RepositoryCache(root, { install: async (directory) => {
attempts += 1
if (attempts === 1) throw new Error('install failed')
await fakePackage(directory)
} })
await expect(cache.resolve('github:owner/repository#ref')).rejects.toThrow('failed to prepare repository')
expect(await readdir(root)).toEqual([])
await expect(cache.resolve('github:owner/repository#ref')).resolves.toContain('node_modules')
expect(attempts).toBe(2)
})
it('rejects empty or padded specifiers before touching the cache', async () => {
const root = await temporaryRoot('repository-input')
const cache = new RepositoryCache(root, { install: fakePackage })
expect(() => cache.resolve('')).toThrow('non-empty unpadded string')
expect(() => cache.resolve(' github:owner/repository#ref')).toThrow('non-empty unpadded string')
await expect(readdir(root)).resolves.toEqual([])
})
it('fails loud on a corrupt published marker instead of reinstalling it', async () => {
const root = await temporaryRoot('repository-corrupt')
const specifier = 'github:owner/repository#corrupt'
const key = createHash('sha256').update(specifier).digest('hex')
const entry = join(root, key)
await mkdir(join(entry, 'node_modules', 'repository'), { recursive: true })
await writeFile(join(entry, '.repository-cache.json'), '{}\n')
const cache = new RepositoryCache(root, { install: async () => { throw new Error('must not reinstall') } })
await expect(cache.resolve(specifier)).rejects.toThrow('repository cache marker is invalid')
})
it('isolates and prepares a .dsh-plugin Git subpath from an enclosing pnpm workspace', { timeout: 60_000 }, async () => {
const root = await temporaryRoot('repository-pnpm')
const repository = join(root, 'source')
await mkdir(join(repository, '.dsh-plugin'), { recursive: true })
await mkdir(join(repository, '.dsh-plugin', 'build-helper'), { recursive: true })
await mkdir(join(repository, '.dsh-plugin', 'prepare-helper'), { recursive: true })
await mkdir(join(repository, 'skills', 'fixture'), { recursive: true })
const shadowPnpm = join(root, 'shadow-pnpm')
await mkdir(shadowPnpm)
await writeFile(join(shadowPnpm, 'pnpm'), '#!/bin/sh\nexit 99\n', { mode: 0o700 })
await writeFile(join(shadowPnpm, 'pnpm.bat'), '@exit /b 99\r\n')
await writeFile(join(repository, 'package.json'), `${JSON.stringify({
name: 'repository-fixture',
private: true,
version: '1.0.0',
packageManager: `pnpm@${BUNDLED_PNPM_VERSION}`,
})}\n`)
await writeFile(join(repository, 'pnpm-workspace.yaml'), 'packages: []\n')
await writeFile(join(repository, 'pnpm-lock.yaml'), [
"lockfileVersion: '9.0'",
'settings:',
' autoInstallPeers: true',
' excludeLinksFromLockfile: false',
'importers:',
' .: {}',
'',
].join('\n'))
await writeFile(join(repository, '.dsh-plugin', 'build-helper', 'package.json'), `${JSON.stringify({
name: 'repository-build-helper',
version: '1.0.0',
bin: 'index.js',
})}\n`)
await writeFile(join(repository, '.dsh-plugin', 'build-helper', 'index.js'), [
'#!/usr/bin/env node',
"require('node:fs').writeFileSync('dependency-built.txt', 'dependency available\\n')",
'',
].join('\n'), { mode: 0o700 })
await writeFile(join(repository, '.dsh-plugin', 'prepare-helper', 'package.json'), `${JSON.stringify({
name: 'repository-prepare-helper',
version: '1.0.0',
bin: { 'dsh-plugin-prepare': 'index.js' },
})}\n`)
await writeFile(join(repository, '.dsh-plugin', 'prepare-helper', 'index.js'), [
'#!/usr/bin/env node',
"const { cpSync, mkdirSync, writeFileSync } = require('node:fs')",
"mkdirSync('dsh-plugin-assets/skills', { recursive: true })",
"cpSync('../skills', 'dsh-plugin-assets/skills/0', { recursive: true })",
"writeFileSync('dsh-plugin.mjs', 'export function apply() {}\\n')",
"writeFileSync('prepared.txt', `${process.env.REPOSITORY_TEST_VISIBLE ?? 'absent'}|${process.env.REPOSITORY_TEST_TOKEN ?? 'absent'}|${process.env.PNPM_CONFIG_IGNORE_WORKSPACE ?? 'absent'}\\n`)",
"writeFileSync('environment.json', `${JSON.stringify({ path: process.env.PATH, pathExt: process.env.PATHEXT })}\\n`)",
'',
].join('\n'), { mode: 0o700 })
await writeFile(join(repository, 'skills', 'fixture', 'SKILL.md'), 'repository skill source\n')
await writeFile(join(repository, '.dsh-plugin', 'package.json'), `${JSON.stringify({
name: 'repository-plugin-fixture',
version: '1.0.0',
scripts: {
// The fixture owns dependency installation, not platform-specific
// node_modules/.bin shim generation during pnpm's Git preparation.
prepack: [
'node ./node_modules/repository-build-helper/index.js',
'node ./node_modules/repository-prepare-helper/index.js',
].join(' && '),
},
devDependencies: {
'repository-build-helper': 'file:./build-helper',
'repository-prepare-helper': 'file:./prepare-helper',
},
dsh: { skills: ['../skills'] },
})}\n`)
await execFileAsync('git', ['init', '--quiet'], { cwd: repository })
await execFileAsync('git', ['add', '.'], { cwd: repository })
await execFileAsync('git', [
'-c', 'user.name=Repository Fixture',
'-c', 'user.email=repository@example.invalid',
'commit', '--quiet', '-m', 'fixture',
], { cwd: repository })
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repository, encoding: 'utf8' })
const specifier = `git+${pathToFileURL(repository).href}#${stdout.trim()}&path:/.dsh-plugin`
vi.stubEnv('REPOSITORY_TEST_VISIBLE', 'visible')
vi.stubEnv('REPOSITORY_TEST_TOKEN', 'hidden')
vi.stubEnv('PNPM_HOME', shadowPnpm)
vi.stubEnv('PATH', [shadowPnpm, ...(process.env.PATH === undefined ? [] : [process.env.PATH])].join(delimiter))
vi.stubEnv('PATHEXT', '.BAT;.CMD;.EXE')
const installed = await new RepositoryCache(join(root, 'cache')).resolve(specifier)
await expect(readFile(join(installed, 'dependency-built.txt'), 'utf8')).resolves.toBe('dependency available\n')
await expect(readFile(join(installed, 'prepared.txt'), 'utf8')).resolves.toBe('visible|absent|true\n')
const environment = JSON.parse(await readFile(join(installed, 'environment.json'), 'utf8')) as {
path: string
pathExt: string
}
expect(environment.path.split(delimiter)).not.toContain(shadowPnpm)
expect(environment.pathExt.split(';')[0]?.toUpperCase()).toBe('.CMD')
await expect(readFile(join(installed, 'dsh-plugin.mjs'), 'utf8')).resolves.toContain('export function apply')
expect(lf(await readFile(join(installed, 'dsh-plugin-assets/skills/0/fixture/SKILL.md'), 'utf8')))
.toBe('repository skill source\n')
await expect(readFile(join(installed, 'package.json'), 'utf8'))
.resolves.toContain('repository-plugin-fixture')
})
})

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/bundle/base/README.md
README.md: 2a87b01ad4819750a58163f8c472e61ea633588e
README.zh.md: dc79895355546812aa3371487190724f169c6260
README.md: 70ecc181da8f0c120b8da0d55f68d47bf22d5820
README.zh.md: 11f10bf561429c11471ff57d08950677e4924b40

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, repository Plugins, telemetry — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code.
The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, and telemetry — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code.
Windows hosts booting a shipped profile additionally receive [`windows.cordis.patch.yml`](windows.cordis.patch.yml): it disables the POSIX-only bash stack (`bash-sandbox`/`tool-bash`) and inserts the sandbox-confined PowerShell stack (`@deepseek-ai/dsh-pwsh-sandbox`, `@deepseek-ai/dsh-tool-pwsh`). The permission surface stays exactly as on POSIX: `sandbox`/`sandbox-policy` enforce the file-effect policy through the Windows ACL restricted-token runner (the win32 chain of `dsh-sandbox-local``@deepseek-ai/dsh-sandbox-windows-acl`), the permission switcher and the approval service run unchanged, and `fs-sandbox` keeps fencing `ctx.fs` writes — mounting `dsh-fs-local` alongside it would double-register `ctx.fs` and fail the load. The launcher applies the layer between the bundle layers and the user layers on win32 hosts; a Windows host that prefers the unconfined local pwsh executor or full access overrides these rows through its profile or home `cordis.patch.yml` (the bash-restore recipe must be complete: disable `pwsh-sandbox`/`tool-pwsh` AND re-enable `bash-sandbox`/`tool-bash` — both executor families register the same `bash` service, so an incomplete recipe fails loud at load). POSIX hosts never receive it.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.md) 选择、工具、持久化、策略、settingscredentials、repository 插件、遥测——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 APIprofile 组合器通过 manifest元数据清单`dsh.bundle.patch` 字段解析 patch绝不通过代码。
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.md) 选择、工具、持久化、策略、settingscredentials遥测——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 APIprofile 组合器通过 manifest元数据清单`dsh.bundle.patch` 字段解析 patch绝不通过代码。
启动交付 profile 的 Windows 主机还会额外收到 [`windows.cordis.patch.yml`](windows.cordis.patch.yml):它禁用仅 POSIX 的 bash 栈(`bash-sandbox`/`tool-bash`),并插入沙盒受限的 PowerShell 栈(`@deepseek-ai/dsh-pwsh-sandbox``@deepseek-ai/dsh-tool-pwsh`)。权限面与 POSIX 完全一致:`sandbox`/`sandbox-policy` 通过 Windows ACL 受限令牌 runner`dsh-sandbox-local` 的 win32 链 → `@deepseek-ai/dsh-sandbox-windows-acl`)执行文件效果策略,权限切换器与 approval 服务原样运行,`fs-sandbox` 继续围栏 `ctx.fs` 写入——在其旁再挂载 `dsh-fs-local` 会重复注册 `ctx.fs` 并在加载时失败。启动器在 win32 主机上把该层应用于 bundle 层与用户层之间;偏好不限权本地 pwsh 执行器或完整访问的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行bash 恢复配方必须完整:禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`——两个执行器家族注册同一个 `bash` 服务,配方不完整会在加载时 fail loud。POSIX 主机永远不会收到它。

View File

@@ -21,13 +21,6 @@
config:
root: ['.']
# The profile's cordis.patch.yml replaces this row's config to select exact GitHub
# repository Plugin generations. The app registers the DSH-owned runtime even
# when the list is empty so a later personal-config edit can load
# transactionally; one-shot headless runs consume the startup value only.
- id: repository-plugins
name: '@deepseek-ai/dsh-repository-plugin'
- id: llm
name: '@deepseek-ai/dsh-llm'

View File

@@ -63,7 +63,6 @@
"@deepseek-ai/dsh-plan-mode": "workspace:^",
"@deepseek-ai/dsh-pwsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-repeat-tool-guard": "workspace:^",
"@deepseek-ai/dsh-repository-plugin": "workspace:^",
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",

View File

@@ -33,14 +33,8 @@ export const inject = ['tools']
/** Default timeout for individual MCP tool calls (ms). */
const DEFAULT_TOOL_CALL_TIMEOUT_MS = 60_000
/**
* Valid `serverName`: 132 chars of `[A-Za-z0-9_-]`. Kept well under the
* 64-char public-name budget so typical raw tool names survive unhashed.
* Exported so upstream producers of Config inputs (repository-plugin's
* `.mcp.json` prepare-time validation) reject the same names this registry
* would.
*/
export const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/
/** Valid `serverName`, kept below the public tool-name budget. */
const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/
/**
* Live `serverName` reservations per app, keyed off `ctx.root` (multiple apps

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/self-modification/README.md
README.md: c94f9ac9a79709e418448d9e440c38296e759335
README.zh.md: 44b5d9ff4cb09fb4a4e44f446894c9925a62ec51
README.md: 2f409f779ae32c9eedc9c57dbb6476c0639205ca
README.zh.md: 09c34c18b66803c544d7a572820c2001405fb6dd

View File

@@ -2,9 +2,8 @@
English | [中文](README.zh.md)
Model-facing tools over the live cordis runtime the agent itself runs inside: inspect the loaded plugins and service surface, mount model-written plugins, and dispose them again — plus the restricted repository Plugin runtime. Design home: [the toolset Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md).
Model-facing tools over the live cordis runtime the agent itself runs inside: inspect the loaded plugins and service surface, mount model-written plugins, and dispose them again. The group is the landing zone for future self-modification packages. Design home: [the toolset Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md).
| Package | Role | ctx key |
|---|---|---|
| [`tool-cordis/`](tool-cordis/README.md) | Model-facing runtime inspection and temporary-plugin tools | registers on `ctx.tools` |
| [`repository-plugin/`](repository-plugin/README.md) | Repository skill and MCP composition | registers a Loader builtin |

View File

@@ -2,9 +2,8 @@
[English](README.md) | 中文
agent 修改自身运行时:检查已加载的插件与服务接口、挂载模型编写的插件并再次 dispose,外加受限 repository Plugin 运行时。设计居所:[工具集 Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)。
agent 修改自身运行时:检查已加载的插件与服务接口、挂载模型编写的插件并再次 dispose。该组是未来自我修改类包的落点。设计居所:[工具集 Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)。
| 包package | 角色 | ctx 键 |
|---|---|---|
| [`tool-cordis/`](tool-cordis/README.md) | `cordis_inspect``cordis_mount``cordis_unmount` 工具:读取当前进程运行时,并在一个自有分组 fiber 下管理内存中的临时插件 | 注册到 `ctx.tools` |
| [`repository-plugin/`](repository-plugin/README.md) | 通过 DSH 自有子 Plugin 准备并挂载静态 repository skills 与通用 `.mcp.json` server | 注册一个 Loader builtin |

View File

@@ -1,6 +0,0 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/self-modification/repository-plugin/README.md
README.md: 666f00e02b9ab33bff348df6b4ff90e3f3bfecc7
README.zh.md: b09f68bc17a4eb08df6ecbb3782e14bf26fb7d7f

View File

@@ -1,128 +0,0 @@
# @deepseek-ai/dsh-repository-plugin
English | [中文](README.zh.md)
Trusted repository package format for DeepSeek Harness. A `.dsh-plugin` npm package may contribute a compiled Cordis/DSH Plugin entry, skill roots, and a common `.mcp.json`; its ordinary `prepack` lifecycle owns dependency installation and source compilation before the DSH prepare helper validates the outputs and emits the Loader wrapper. Static contributions compose [`dsh-skill-local`](../../skill/skill-local/README.md) and [`dsh-mcp-client`](../../mcp/mcp-client/README.md). Design rationale: [trusted repository package code](../../../.agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.md) and the [static contribution subformat](../../../.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.md).
## Authoring format
Place an ordinary package in the repository's `.dsh-plugin` directory:
```json
{
"name": "humanize-dsh-plugin",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"prepack": "npm run build && dsh-plugin-prepare"
},
"dsh": {
"entry": "./lib/plugin.js",
"skills": ["../skills"],
"mcpServers": "../.mcp.json"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.29.0"
},
"devDependencies": {
"@deepseek-ai/dsh-repository-plugin": "^0.0.1",
"typescript": "6.0.3"
}
}
```
`scripts.prepack` must be non-empty and invoke `dsh-plugin-prepare`; it may run arbitrary package-owned build steps first. The package declares `@deepseek-ai/dsh-repository-plugin` as an ordinary development dependency so its published executable is available to that lifecycle. DSH does not inject the helper: the repository package declares and runs its own compiler, runtime dependencies, preparation helper, and other npm lifecycle code. The selected package is installed from its own manifest instead of inheriting an enclosing pnpm workspace, so declare every dependency it needs and do not depend on workspace-only hoisting. DSH does not transpile TypeScript or infer a package entry.
`dsh.entry` is an optional relative path to a compiled ESM Cordis Plugin inside `.dsh-plugin`. The module may use either namespace exports or a default export and owns its ordinary `name`, `inject`, `Config`, registrations, and effects. `dsh.skills` is an optional array of local skill roots, and `dsh.mcpServers` is an optional path to one `.mcp.json`; at least one of the three fields is required. Skill and MCP paths may reach adjacent repository assets but must remain beneath the directory containing `.dsh-plugin`; the compiled entry must remain inside the package selected and packed by the package manager. A repository containing several Plugins gives each one its own `.dsh-plugin` package under a different selectable subdirectory.
The repository package and every dependency or lifecycle script it runs are trusted code, just like an npm package selected directly by the user. This format is not a sandbox: install only repositories whose code may access the host process, filesystem, network, and services declared through Cordis. Exact refs and the immutable cache provide identity and reproducibility, not isolation.
## Standalone app configuration
The shipped `dsh-base` bundle every profile starts from contains an empty `repository-plugins` row. A user enables exact GitHub generations by replacing that row's config in a user patch layer — `$DSH_HOME/profiles/<name>/cordis.patch.yml`, or the home-level `$DSH_HOME/cordis.patch.yml` shared by every profile; a `--patch` overlay patches the same row for one run:
```yaml
- id: repository-plugins
name: '@deepseek-ai/dsh-repository-plugin'
config:
repositories:
- 'github:PolyArch/humanize#<commit>'
- 'github:owner/repository#<ref>&path:/plugins/one/.dsh-plugin'
```
Each source must use `github:owner/repository#<ref>`. Omitting `&path:` selects `/.dsh-plugin`; an explicit path is absolute within the repository and must end in `.dsh-plugin`. A commit ref gives the clearest immutable identity, while tags and branches remain accepted exact config values. `cacheDir` may override the default `$DSH_HOME/cache/repository-plugins` cache root.
Git transport uses the host's ordinary Git authentication. Public repositories need no credentials; private sources require a read-only credential or SSH agent that can read the selected repository. DSH removes credential-shaped environment variables before package lifecycles, so configure Git itself, such as through a credential helper or job-scoped Git config, instead of expecting an exported token variable to cross that boundary. Repository lifecycle code is trusted and can invoke Git, so use the narrowest repository-scoped credential available.
Long-lived surfaces watch both `cordis.patch.yml` layers through Cordis HMR. A valid source-list change installs and swaps the complete repository Plugin generation; a failed fetch, prepare, import, or Plugin application keeps the last good tree and broadcasts `hmr/config-update-failed(filename, error)`. One-shot runs read the layers only at startup, and a `--patch` overlay is never watched. An identical source string permanently reuses its prepared cache entry, so selecting changed code requires a ref, path, or other source-config change. App integration rationale: [config-only repository Plugins Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.md).
## Preparation
During exact Git installation, DSH's bundled pnpm installs the selected package from its own manifest. A transaction-owned `pnpm` wrapper reinvokes the same pinned pnpm with `--ignore-workspace`, so an enclosing workspace lockfile cannot suppress dependencies declared only by the selected `.dsh-plugin` package. The required `prepack` lifecycle runs after that dependency installation and before the selected subdirectory is packed; its ordinary `node_modules/.bin` lookup obtains `dsh-plugin-prepare` from the declared direct development dependency on `@deepseek-ai/dsh-repository-plugin`. That package marks its Cordis/DSH runtime peers optional so using the executable alone does not install the runtime graph. Package-owned commands may build TypeScript or other source before invoking the helper. The helper validates `package.json#dsh`, verifies that the compiled entry is an in-package file, validates skill and MCP sources, copies static assets under `dsh-plugin-assets`, and writes `dsh-plugin.mjs`. Before importing that wrapper, DSH revalidates that the installed package retained both the direct development dependency and a `prepack` declaration containing the helper command. Failure to resolve the published helper, install dependencies, build, or prepare fails before a cache generation is published. Rationale: [npm-backed Git source preparation Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md).
## Runtime composition
Loading this package registers one effect-scoped Loader builtin. Each generated wrapper delegates its prepared static manifest to that builtin, then imports and mounts `dsh.entry` when declared. The wrapper can statically gate only the `loader`, `skills`, and `tools` services implied by the prepared manifest; the entry's own `inject` is discovered when that child is mounted. The entry must reach `ACTIVE`, so a missing entry-only service or startup failure rejects the repository generation instead of committing an inert child, and all effects disappear on Loader removal or rollback. The runtime likewise validates every declared skill root as an existing in-package directory before mounting — a package whose generated outputs were dropped by `files`/`.npmignore` or damaged in cache fails instead of silently losing contributions. Repository skill roots mount as uniquely named `dsh-skill-local` providers with default project/user roots excluded and watching disabled; cached package generations are immutable.
## Common MCP format
The `.mcp.json` root is `{ "mcpServers": { ... } }`. A stdio entry accepts only `type: "stdio"` (optional), `command`, `args`, and `env`; an HTTP entry accepts only `type: "http"`, `url`, and `headers`. String values support exact `${NAME}` process-environment expansion at Plugin load, and a missing name fails that load. HTTP URLs become the existing MCP client's `streamable-http` transport; stdio entries use the prepared package directory as `cwd`.
Unknown fields reject, including OAuth and `auth` objects. There is no `CLAUDE_PLUGIN_ROOT` expansion or compatibility layer. After translation, the existing `dsh-mcp-client` exclusively owns transport creation, connection diagnostics, tool synchronization, calls, and disconnect lifecycle. Repository-declared servers enable its strict startup mode: Plugin activation waits for the initial connection and tool synchronization, so the first model request observes a fully registered initial tool generation, while a network, child-process, discovery, or registration failure rejects the candidate repository generation instead of silently activating without its declared tools.
## Export shape
Namespace Plugin: named exports `name` / `inject` / `apply`, preparation constants, and `prepareDshPlugin`; no default export. The package also exposes the `dsh-plugin-prepare` executable and an invariant companion.
## Model Experience
### Repository skills
#### What the model sees
Indirectly through `dsh-tool-skill`: prepared, model-invocable skills join its logged catalog and selected instruction-body surface under their declared names and descriptions. The exact consumer schema is in the generated [`skill` tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-skill).
#### Token effect
Conditional and data-dependent: each visible repository skill adds one capped catalog row; loading one adds its full current instruction body and resource-base guidance to retained tool history.
#### KV Cache effect
A stable prepared Plugin set is prefix-stable. Adding, removing, or replacing a repository Plugin can append the consumer's replacement catalog and affect later request prefixes.
### Repository MCP tools
#### What the model sees
Indirectly through `dsh-mcp-client`: every connected server contributes its server-qualified tool schemas, and calls retain that client's canonical MCP results and rendering.
#### Token effect
Conditional on successful connection and the remote tool list; schemas recur on requests in the active tool view, while calls and results remain in history until compaction.
#### KV Cache effect
Stable connected tool lists are prefix-stable. Plugin lifecycle or MCP tool-list changes can change later tool-schema prefixes from the first affected definition.
### Repository code
#### What the model sees
Data-dependent. The trusted Cordis entry may contribute any DSH behavior available through its declared services and events, including tools, prompt sections, policies, commands, and transformations. Every model-visible contribution remains subject to its owning DSH seam's logging and lifecycle contract.
#### Token effect
Defined by the services and registrations the entry contributes; the repository format itself adds no model content.
#### KV Cache effect
Stable registrations preserve the owning surface's normal prefix behavior. Loading, removing, or replacing the exact repository generation can change any prefixes affected by that Plugin.
## Known Limitations and Deferred Work
- **No code sandbox** — `dsh.entry`, npm dependencies, and package lifecycle scripts execute with the DSH host's authority; repository trust is mandatory.
- **Entry-only service dependencies are not pre-gated** — the generated wrapper cannot declare an entry module's `inject` before importing it. Any service beyond those implied by Skills or MCP must already exist when the wrapper mounts the entry, or that repository generation rejects.
- **No MCP authentication protocol** — static headers may use environment expansion, but OAuth-bearing definitions reject and private-server login flows are not implemented here.
- **Generated assets are immutable runtime input** — repository cache generations are not watched; source, ref, path, or configuration must select another prepared generation.

View File

@@ -1,128 +0,0 @@
# @deepseek-ai/dsh-repository-plugin
[English](README.md) | 中文
这是 DeepSeek Harness 的受信任 repository 包格式。`.dsh-plugin` NPM 包可以贡献已编译的 CordisDSH 插件入口、skill技能根和通用 `.mcp.json`;其常规 `prepack` 生命周期负责安装依赖并编译源码,随后 DSH 准备辅助程序校验输出并生成 Loader 包装层。静态贡献由 [`dsh-skill-local`](../../skill/skill-local/README.md) 与 [`dsh-mcp-client`](../../mcp/mcp-client/README.md) 组合。设计依据见[受信任 repository 包代码](../../../.agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.md)和[静态贡献子格式](../../../.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.md)。
## 创作格式
在仓库的 `.dsh-plugin` 目录中放置一个普通包:
```json
{
"name": "humanize-dsh-plugin",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"prepack": "npm run build && dsh-plugin-prepare"
},
"dsh": {
"entry": "./lib/plugin.js",
"skills": ["../skills"],
"mcpServers": "../.mcp.json"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.29.0"
},
"devDependencies": {
"@deepseek-ai/dsh-repository-plugin": "^0.0.1",
"typescript": "6.0.3"
}
}
```
`scripts.prepack` 必须非空并调用 `dsh-plugin-prepare`;可以先运行任意包自有的构建步骤。包将 `@deepseek-ai/dsh-repository-plugin` 声明为普通开发依赖使该生命周期可以使用其已发布的可执行文件。DSH 不会注入辅助程序repository 包自行声明并运行编译器、运行时依赖、准备辅助程序及其他 NPM 生命周期代码。所选包按自身 manifest 独立安装,而不继承外层 pnpm workspace因此必须声明所需的每项依赖不能依赖仅由 workspace 提升而可见的包。DSH 不转译 TypeScript也不推断包入口。
`dsh.entry` 是指向 `.dsh-plugin` 内已编译 ESM Cordis 插件的可选相对路径。该模块可以使用 namespace 导出或 default export并自行拥有常规的 `name``inject``Config`、注册和 effect。`dsh.skills` 是可选的本地 skill 根数组,`dsh.mcpServers` 是指向一个 `.mcp.json` 的可选路径三个字段中至少声明一个。skill 和 MCP 路径可以引用相邻的 repository 资源,但必须留在包含 `.dsh-plugin` 的目录下;已编译入口必须留在由包管理器选中并打包的包内。一个仓库可以在不同的可选择子目录下放置多个各自独立的 `.dsh-plugin` 包。
repository 包及其运行的每项依赖或生命周期脚本都是受信任代码,与用户直接选择的 NPM 包相同。本格式不是沙箱:只有在你信任仓库代码并愿意允许其访问宿主进程、文件系统、网络及其通过 Cordis 声明的服务时才应安装。精确 ref 和不可变缓存提供身份与可复现性,而非隔离。
## 独立应用配置
随附的 `dsh-base` 组合包是每个 profile 的起点,其中包含一个空 `repository-plugins` 配置项。用户可在用户 patch 层中替换该配置项的配置来启用精确指定的 GitHub generation写入 `$DSH_HOME/profiles/<name>/cordis.patch.yml`,或写入各 profile 共享的 home 级 `$DSH_HOME/cordis.patch.yml``--patch` overlay 则只为单次运行 patch 同一配置项:
```yaml
- id: repository-plugins
name: '@deepseek-ai/dsh-repository-plugin'
config:
repositories:
- 'github:PolyArch/humanize#<commit>'
- 'github:owner/repository#<ref>&path:/plugins/one/.dsh-plugin'
```
每个源都必须采用 `github:owner/repository#<ref>`。省略 `&path:` 时选择 `/.dsh-plugin`;显式路径是仓库内的绝对路径,并且必须以 `.dsh-plugin` 结尾。commit ref 提供最清晰的不可变身份tag 和 branch 仍可作为精确配置值使用。`cacheDir` 可覆盖默认缓存根 `$DSH_HOME/cache/repository-plugins`
Git 传输使用宿主的常规 Git 认证。公共仓库无需凭据;私有源需要可读取所选仓库的只读凭据或 SSH agent。DSH 会在包生命周期运行前移除名称符合凭据模式的环境变量,因此请配置 Git 本身,例如使用 Git 凭据辅助工具或作业作用域的 Git 配置,而不要指望已导出的 token 变量跨越该边界。仓库生命周期代码受信任且可以调用 Git因此请使用作用域最窄且仅限所选仓库的凭据。
长期运行的 surface 通过 Cordis HMR热模块替换监视两个 `cordis.patch.yml` 层。有效的源列表变更会安装并替换整套 repository Plugin generation拉取、准备、导入或插件应用失败时最后一个可用树保持运行并广播 `hmr/config-update-failed(filename, error)`。一次性运行只在启动时读取这些层,`--patch` overlay 则从不被监视。相同的源字符串会永久复用其已准备缓存条目,因此必须改变 ref、路径或其他源配置才能选择发生变化的代码。应用集成依据见[仅凭配置接入 repository Plugin 的 Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.md)。
## 准备阶段
安装精确指定的 Git 源时DSH 随附的 pnpm 会按所选包自身的 manifest 安装。由事务持有的 `pnpm` 包装脚本会以 `--ignore-workspace` 重新调用同一份锁定的 pnpm因此外层 workspace lockfile 无法抑制仅由所选 `.dsh-plugin` 包声明的依赖。必需的 `prepack` 生命周期在该依赖安装完成后、选定子目录打包前运行;其常规 `node_modules/.bin` 查找会从直接声明的 `@deepseek-ai/dsh-repository-plugin` 开发依赖中取得 `dsh-plugin-prepare`。该包把 CordisDSH 运行时对等依赖peer dependency标为可选因此单独使用该可执行文件不会安装运行时依赖图。包自有命令可以在调用辅助程序前构建 TypeScript 或其他源码。辅助程序会校验 `package.json#dsh`,确认已编译入口是包内文件,校验 skill 与 MCP 源,把静态资源复制到 `dsh-plugin-assets`,并写入 `dsh-plugin.mjs`。导入该包装层前DSH 会重新校验已安装包是否仍同时保留该直接开发依赖,以及包含该辅助命令的 `prepack` 声明。无法解析已发布的辅助程序,或安装依赖、构建或准备失败时,流程会在发布缓存 generation 前失败。设计依据见[基于 NPM 的 Git 源准备 Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md)。
## 运行时组合
加载本包会注册一个 effect-scoped Loader builtin。每个生成的包装层都把已准备的静态 manifest元数据清单委托给该 builtin再在声明了 `dsh.entry` 时导入并挂载该入口。包装层只能静态门控已准备 manifest 所隐含的 `loader``skills``tools` 服务;入口自身的 `inject` 要到挂载该子级时才会发现。入口必须进入 `ACTIVE`,因此缺少入口专用服务或启动失败时,会拒绝 repository generation而不会提交未激活的子级Loader 移除或回滚时,所有 effect 都会消失。运行时同样会在挂载前校验每个声明的 skill 根都是包内实际存在的目录——生成输出因 `files``.npmignore` 被丢弃或在缓存中损坏的包会加载失败而不是静默丢失贡献。Repository skill 根以唯一命名的 `dsh-skill-local` 提供方挂载,排除默认项目/用户根并禁用监视;缓存包 generation 是不可变的。
## 通用 MCP 格式
`.mcp.json` 根对象是 `{ "mcpServers": { ... } }`。stdio 条目只接受可选的 `type: "stdio"``command``args``env`HTTP 条目只接受 `type: "http"``url``headers`。字符串值在插件加载时支持严格的 `${NAME}` 进程环境变量展开缺失变量会使该次加载失败。HTTP URL 映射到现有 MCP client 的 `streamable-http` transportstdio 条目以已准备的包目录作为 `cwd`
未知字段会被拒绝,包括 OAuth 字段与 `auth` 对象。不提供 `CLAUDE_PLUGIN_ROOT` 展开或兼容层。完成格式转换后,现有 `dsh-mcp-client` 独占 transport 创建、连接诊断、工具同步、调用和断开生命周期。Repository 声明的 server 会启用其严格启动模式:插件激活会等待初始连接与工具同步,因此首个模型请求会看到已完整注册的初始工具 generation网络、子进程、发现或注册失败则会拒绝候选 repository generation而不是在缺少已声明工具的情况下静默激活。
## 导出形状
Namespace 插件:具名导出 `name``inject``apply`、准备阶段常量和 `prepareDshPlugin`,不提供 default export。本包还提供 `dsh-plugin-prepare` 可执行文件和 invariant companion。
## 模型体验
### Repository skill
#### 模型看到什么
通过 `dsh-tool-skill` 间接呈现:已准备且允许模型调用的 skill 会按其声明的名称和描述进入该消费方记录到日志的目录及所选指令正文表面。消费方的确切 schema 见生成的 [`skill` 工具目录](../../../docs/tool-catalog.md#deepseek-aidsh-tool-skill)。
#### Token 影响
有条件且随数据变化:每个可见的 repository skill 增加一行受限长度的目录项;加载一个 skill 会把其当前完整指令正文和资源基址指引加入保留的工具历史。
#### KV Cache 影响
稳定的已准备插件集合保持前缀稳定。添加、移除或替换 repository 插件可能使消费方追加替换目录,并影响后续请求前缀。
### Repository MCP 工具
#### 模型看到什么
通过 `dsh-mcp-client` 间接呈现:每个已连接 server 都贡献带 server 限定名的工具 schema调用会保留该 client 的规范 MCP 结果和渲染。
#### Token 影响
取决于连接成功和远端工具列表schema 会在当前工具视图中的请求上重复出现而调用与结果会留在历史中直至压缩compaction
#### KV Cache 影响
稳定的已连接工具列表保持前缀稳定。插件生命周期或 MCP 工具列表变化可能从首个受影响定义开始改变后续工具 schema 前缀。
### Repository 代码
#### 模型看到什么
取决于数据。受信任的 Cordis 入口可以通过其声明的服务和事件贡献任意可用的 DSH 行为,包括工具、提示词片段、策略、命令和转换。每项模型可见贡献仍受所属 DSH seam 的日志与生命周期约定约束。
#### Token 影响
由入口贡献的服务和注册决定repository 格式本身不添加模型内容。
#### KV Cache 影响
稳定的注册会保留所属表面的正常前缀行为。加载、移除或替换精确的 repository generation可能改变受该插件影响的任意前缀。
## 已知限制与暂缓事项
- **没有代码沙箱**`dsh.entry`、NPM 依赖和包生命周期脚本以 DSH 宿主权限执行;必须信任该 repository。
- **入口专用服务依赖不会预先门控**:生成的包装层无法在导入入口模块前声明其 `inject`。除 skill 或 MCP 隐含的服务外,其他任何服务在包装层挂载入口时都必须已经存在,否则该 repository generation 会被拒绝。
- **没有 MCP 认证协议**:静态 header 可以使用环境变量展开,但带 OAuth 的定义会被拒绝,私有 server 登录流程不在此实现。
- **生成资源是不可变运行时输入**repository cache generation 不受监视;必须改变 source、ref、path 或配置才能选择另一份已准备 generation。

View File

@@ -1,73 +0,0 @@
{
"name": "@deepseek-ai/dsh-repository-plugin",
"description": "Trusted repository package format and Cordis runtime for DeepSeek Harness",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"bin": {
"dsh-plugin-prepare": "./lib/bin.js"
},
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/bin.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-mcp-client": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"@deepseek-ai/dsh-skill-local": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"peerDependenciesMeta": {
"@cordisjs/plugin-loader": {
"optional": true
},
"@deepseek-ai/dsh-invariants": {
"optional": true
},
"@deepseek-ai/dsh-mcp-client": {
"optional": true
},
"@deepseek-ai/dsh-paths": {
"optional": true
},
"@deepseek-ai/dsh-skill-local": {
"optional": true
},
"cordis": {
"optional": true
}
},
"dependencies": {
"zod": "^4.4.3"
},
"devDependencies": {
"@cordisjs/plugin-loader": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-mcp-client": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-local": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -1,12 +0,0 @@
#!/usr/bin/env node
/** Command-line entry that prepares the current `.dsh-plugin` package. @module */
import { prepareDshPlugin } from './format.ts'
try {
await prepareDshPlugin()
} catch (error) {
process.stderr.write(`dsh-plugin-prepare: ${error instanceof Error ? error.message : String(error)}\n`)
process.exitCode = 1
}

View File

@@ -1,249 +0,0 @@
/**
* Trusted repository-package preparation and prepared-manifest validation.
* @module
*/
import { cp, copyFile, mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises'
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
import { z } from 'zod'
import { parseMcpDocument } from './mcp.ts'
/** Fixed module filename loaded from an installed prepared plugin package. */
export const PREPARED_ENTRY_FILENAME = 'dsh-plugin.mjs'
/** Fixed directory containing copied static plugin assets. */
export const PREPARED_ASSET_DIRECTORY = 'dsh-plugin-assets'
/** Loader builtin used by every generated repository wrapper. */
export const REPOSITORY_PLUGIN_BUILTIN = 'dsh-repository-plugin'
/** Dependency-provided command that repository package `prepack` lifecycles must invoke. */
export const REPOSITORY_PLUGIN_PREPARE_COMMAND = 'dsh-plugin-prepare'
/** Published package whose direct development dependency supplies the prepare command. */
export const REPOSITORY_PLUGIN_PACKAGE_NAME = '@deepseek-ai/dsh-repository-plugin'
/**
* Whether a package lifecycle declaration names the preparation dependency's helper.
* @param script - package-authored lifecycle command.
* @returns true when the required helper command is present.
*/
export function hasRepositoryPrepareCommand(script: string): boolean {
return script.includes(REPOSITORY_PLUGIN_PREPARE_COMMAND)
}
const prepackSchema = z.string().min(1).refine(
hasRepositoryPrepareCommand,
{ message: `must invoke ${REPOSITORY_PLUGIN_PREPARE_COMMAND}` },
)
const sourceMetadataSchema = z.object({
skills: z.array(z.string().min(1)).default([]),
mcpServers: z.string().min(1).optional(),
entry: z.string().min(1).optional(),
}).strict().refine(value => value.skills.length > 0 || value.mcpServers !== undefined || value.entry !== undefined, {
message: 'declare at least one skill root, mcpServers file, or compiled entry',
})
const sourcePackageSchema = z.looseObject({
name: z.string().min(1),
devDependencies: z.looseObject({
[REPOSITORY_PLUGIN_PACKAGE_NAME]: z.string().min(1),
}),
scripts: z.looseObject({
prepack: prepackSchema,
}),
dsh: sourceMetadataSchema,
})
const preparedManifestSchema = z.object({
name: z.string().min(1),
skills: z.array(z.string().min(1)),
mcpServers: z.string().min(1).optional(),
entry: z.string().min(1).optional(),
}).strict()
const preparedConfigSchema = z.object({
// Wrappers pass import.meta.url, which is always file: for an installed
// package; any other scheme would only fail later inside fileURLToPath with
// an uncontextualized TypeError, so reject it at this validation boundary.
baseUrl: z.url({ protocol: /^file$/ }),
manifest: preparedManifestSchema,
}).strict()
/** Prepared manifest embedded in the generated wrapper. */
export interface PreparedPluginManifest {
name: string
skills: string[]
mcpServers?: string
entry?: string
}
/** Untrusted generated-wrapper config accepted by the DSH-owned runtime builtin. */
export interface PreparedPluginConfig {
baseUrl: string
manifest: PreparedPluginManifest
}
function formatZodError(label: string, error: z.ZodError): Error {
return new Error(`${label}:\n${z.prettifyError(error)}`)
}
/**
* Validate the config passed by an installed prepared wrapper.
* @param value - wrapper-provided value crossing the file/module boundary.
* @returns a detached typed config.
*/
export function parsePreparedPluginConfig(value: unknown): PreparedPluginConfig {
const result = preparedConfigSchema.safeParse(value)
if (!result.success) throw formatZodError('invalid prepared DSH plugin', result.error)
return {
baseUrl: result.data.baseUrl,
manifest: {
name: result.data.manifest.name,
skills: result.data.manifest.skills,
...result.data.manifest.mcpServers === undefined ? {} : { mcpServers: result.data.manifest.mcpServers },
...result.data.manifest.entry === undefined ? {} : { entry: result.data.manifest.entry },
},
}
}
/**
* Whether `candidate` resolves outside `root` — the containment check shared
* by prepare-time asset copying and runtime prepared-path resolution.
* @param root - directory that must contain the candidate.
* @param candidate - absolute path to test.
* @returns true when the candidate escapes the root.
*/
export function isOutside(root: string, candidate: string): boolean {
const path = relative(root, candidate)
/* v8 ignore next -- Different-drive Windows relative paths cannot be produced on POSIX coverage hosts. */
return path === '..' || path.startsWith(`..${sep}`) || isAbsolute(path)
}
async function sourcePath(pluginDirectory: string, sourceRoot: string, configured: string, kind: 'directory' | 'file'): Promise<string> {
if (isAbsolute(configured)) throw new Error(`DSH plugin asset path must be relative: ${JSON.stringify(configured)}`)
let path: string
try {
path = await realpath(resolve(pluginDirectory, configured))
} catch (cause) {
throw new Error(`DSH plugin asset does not exist: ${JSON.stringify(configured)}`, { cause })
}
if (isOutside(sourceRoot, path)) {
throw new Error(`DSH plugin asset escapes its plugin source root: ${JSON.stringify(configured)}`)
}
const info = await stat(path)
if (kind === 'directory' ? !info.isDirectory() : !info.isFile()) {
throw new Error(`DSH plugin asset is not a ${kind}: ${JSON.stringify(configured)}`)
}
return path
}
function wrapperSource(manifest: PreparedPluginManifest): string {
// The manifest is static, so the wrapper's service dependencies are too:
// declaring them gates the wrapper fiber until the composition provides
// them, which means the runtime's SkillLocal/McpClient children activate
// within the wrapper's own load epoch and their failures (duplicate
// provider names, damaged packages) reject the wrapper's Loader
// transaction instead of leaving a silently PENDING or FAILED child.
const inject = [
'loader',
...manifest.skills.length > 0 ? ['skills'] : [],
...manifest.mcpServers === undefined ? [] : ['tools'],
]
const entryHelpers = manifest.entry === undefined ? [] : [
'function unwrap(exports) {',
' const value = exports?.default ?? exports',
' return value?.__esModule ? (value.default ?? value) : value',
'}',
]
const entryApply = manifest.entry === undefined ? [] : [
' const repositoryPlugin = unwrap(await import(manifest.entry))',
" await mount(ctx, repositoryPlugin, 'repository Plugin entry')",
]
return [
'// Generated by dsh-plugin-prepare. Do not edit.',
`const manifest = ${JSON.stringify(manifest)}`,
'// Value mirror: Cordis const enum FiberState.ACTIVE; keep aligned with dsh-repository-plugin source.ts.',
'const FIBER_ACTIVE = 2',
`export const name = ${JSON.stringify(manifest.name)}`,
`export const inject = ${JSON.stringify(inject)}`,
...entryHelpers,
'async function mount(ctx, plugin, label, config) {',
' const fiber = ctx.plugin(plugin, config)',
' await fiber',
' if (fiber.state !== FIBER_ACTIVE) {',
' const missing = Object.keys(fiber.inject).filter(service => fiber.ctx.get(service) === undefined)',
" throw new Error(`${label} did not activate (waiting for services: ${missing.join(', ') || 'unknown'})`)",
' }',
'}',
'export async function apply(ctx) {',
` const runtime = ctx.loader.builtins[${JSON.stringify(REPOSITORY_PLUGIN_BUILTIN)}]`,
` if (runtime === undefined) throw new Error(${JSON.stringify(`missing Cordis builtin ${REPOSITORY_PLUGIN_BUILTIN}`)})`,
" await mount(ctx, runtime, 'repository Plugin runtime', { baseUrl: import.meta.url, manifest })",
...entryApply,
'}',
'',
].join('\n')
}
/**
* Validate and package one `.dsh-plugin` directory into copied assets plus a generated wrapper.
* Outputs are staged and committed by rename, but the final publish (remove
* old outputs, rename assets, rename entry) is not one atomic step: a crash
* mid-publish can leave assets without an entry or neither. Rerunning prepare
* repairs the package; partial outputs are never importable as a plugin.
* @param directory - `.dsh-plugin` package directory; defaults to the prepare process cwd.
* @returns the generated prepared manifest.
*/
export async function prepareDshPlugin(directory: string = process.cwd()): Promise<PreparedPluginManifest> {
const pluginDirectory = await realpath(resolve(directory))
let packageValue: unknown
try {
packageValue = JSON.parse(await readFile(join(pluginDirectory, 'package.json'), 'utf8')) as unknown
} catch (cause) {
throw new Error(`failed to read DSH plugin package metadata in ${pluginDirectory}`, { cause })
}
const parsed = sourcePackageSchema.safeParse(packageValue)
if (!parsed.success) throw formatZodError('invalid DSH plugin package.json', parsed.error)
const sourceRoot = await realpath(dirname(pluginDirectory))
const skillSources: string[] = []
for (const configured of parsed.data.dsh.skills) {
const source = await sourcePath(pluginDirectory, sourceRoot, configured, 'directory')
if (!isOutside(source, pluginDirectory)) {
throw new Error(`DSH skill root cannot contain the .dsh-plugin package: ${JSON.stringify(configured)}`)
}
skillSources.push(source)
}
let mcpSource: string | undefined
if (parsed.data.dsh.mcpServers !== undefined) {
mcpSource = await sourcePath(pluginDirectory, sourceRoot, parsed.data.dsh.mcpServers, 'file')
parseMcpDocument(await readFile(mcpSource, 'utf8'))
}
let entry: string | undefined
if (parsed.data.dsh.entry !== undefined) {
const entrySource = await sourcePath(pluginDirectory, pluginDirectory, parsed.data.dsh.entry, 'file')
entry = `./${relative(pluginDirectory, entrySource).split(sep).join('/')}`
}
const manifest: PreparedPluginManifest = {
name: parsed.data.name,
skills: skillSources.map((_, index) => `${PREPARED_ASSET_DIRECTORY}/skills/${index}`),
...mcpSource === undefined ? {} : { mcpServers: `${PREPARED_ASSET_DIRECTORY}/.mcp.json` },
...entry === undefined ? {} : { entry },
}
const staging = await mkdtemp(join(pluginDirectory, '.dsh-plugin-prepare-'))
try {
const stagedAssets = join(staging, PREPARED_ASSET_DIRECTORY)
await mkdir(join(stagedAssets, 'skills'), { recursive: true })
await Promise.all(skillSources.map((source, index) => cp(source, join(stagedAssets, 'skills', String(index)), {
recursive: true,
force: false,
errorOnExist: true,
})))
if (mcpSource !== undefined) await copyFile(mcpSource, join(stagedAssets, '.mcp.json'))
await writeFile(join(staging, PREPARED_ENTRY_FILENAME), wrapperSource(manifest))
await rm(join(pluginDirectory, PREPARED_ASSET_DIRECTORY), { recursive: true, force: true })
await rm(join(pluginDirectory, PREPARED_ENTRY_FILENAME), { force: true })
await rename(stagedAssets, join(pluginDirectory, PREPARED_ASSET_DIRECTORY))
await rename(join(staging, PREPARED_ENTRY_FILENAME), join(pluginDirectory, PREPARED_ENTRY_FILENAME))
} finally {
await rm(staging, { recursive: true, force: true })
}
return manifest
}

View File

@@ -1,147 +0,0 @@
/**
* Trusted repository-package runtime for code, skills, and common MCP definitions.
* @module @deepseek-ai/dsh-repository-plugin
*/
import { readFile, stat } from 'node:fs/promises'
import { dirname, isAbsolute, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Context } from 'cordis'
import type {} from '@cordisjs/plugin-loader'
import { RepositoryCache } from '@cordisjs/plugin-loader/repository'
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
import * as McpClient from '@deepseek-ai/dsh-mcp-client'
import { z } from 'zod'
import {
REPOSITORY_PLUGIN_BUILTIN,
isOutside,
parsePreparedPluginConfig,
type PreparedPluginConfig,
} from './format.ts'
import { parseMcpDocument, resolveMcpServers } from './mcp.ts'
import {
loadPreparedRepository,
resolveRepositoryCacheDirectory,
resolveRepositorySpecifier,
} from './source.ts'
export {
PREPARED_ASSET_DIRECTORY,
PREPARED_ENTRY_FILENAME,
REPOSITORY_PLUGIN_BUILTIN,
REPOSITORY_PLUGIN_PACKAGE_NAME,
REPOSITORY_PLUGIN_PREPARE_COMMAND,
prepareDshPlugin,
type PreparedPluginManifest,
} from './format.ts'
/** Cordis plugin name used by Loader diagnostics. */
export const name = 'repository-plugin'
/** Loader service required to register the fixed prepared-wrapper builtin. */
export const inject = ['loader']
/** Repository Plugin runtime and source-list configuration. */
export interface Config {
/** GitHub repository sources with explicit refs and optional `.dsh-plugin` subpaths. */
repositories?: string[]
/** Persistent generation cache; defaults to `$DSH_HOME/cache/repository-plugins`. */
cacheDir?: string
}
export const Config = z.object({
repositories: z.array(z.string().min(1)).default([]),
cacheDir: z.string().min(1).optional(),
}).strict().default({ repositories: [] })
function preparedPath(baseUrl: string, configured: string): string {
if (isAbsolute(configured)) throw new Error(`prepared DSH plugin path must be relative: ${JSON.stringify(configured)}`)
const directory = dirname(fileURLToPath(baseUrl))
const path = resolve(directory, configured)
if (isOutside(directory, path)) {
throw new Error(`prepared DSH plugin path escapes its package: ${JSON.stringify(configured)}`)
}
return path
}
async function preparedDirectory(baseUrl: string, configured: string): Promise<string> {
const path = preparedPath(baseUrl, configured)
// A manifest-declared skill root missing from the installed package (files/
// .npmignore dropping generated outputs, a damaged cache entry) must fail
// the plugin load: the skill provider treats an absent root as legitimately
// empty, which would silently mount a skill-less plugin.
let info
try {
info = await stat(path)
} catch (cause) {
throw new Error(`prepared DSH plugin skill root is missing from the installed package: ${JSON.stringify(configured)}`, { cause })
}
if (!info.isDirectory()) {
throw new Error(`prepared DSH plugin skill root is not a directory: ${JSON.stringify(configured)}`)
}
return path
}
async function applyPrepared(ctx: Context, value: PreparedPluginConfig): Promise<void> {
const config = parsePreparedPluginConfig(value)
const directory = dirname(fileURLToPath(config.baseUrl))
const skillDirectories = await Promise.all(config.manifest.skills.map(path => preparedDirectory(config.baseUrl, path)))
const mcpConfigs = config.manifest.mcpServers === undefined
? []
: resolveMcpServers(
parseMcpDocument(await readFile(preparedPath(config.baseUrl, config.manifest.mcpServers), 'utf8')),
process.env,
directory,
// Schemastery call signatures collapse the parameter to `never` under
// NodeNext; ResolvedMcpServer matches the Config union by design.
).map(input => McpClient.Config(input as never))
await ctx.effect(async function* () {
if (skillDirectories.length > 0) {
const skills = ctx.plugin(SkillLocal, {
providerName: `repository:${config.manifest.name}`,
includeDefaultRoots: false,
customSkillDirs: skillDirectories,
watch: false,
})
await skills
yield skills.dispose
}
for (const mcpConfig of mcpConfigs) {
const mcp = ctx.plugin(McpClient, mcpConfig)
await mcp
yield mcp.dispose
}
}, `repository-plugin(${config.manifest.name})`)
}
const preparedRuntime = {
name: 'repository-plugin-runtime',
apply: applyPrepared,
}
/**
* Register the DSH-owned runtime as the Loader builtin used by fixed prepared wrappers.
* @param ctx - plugin context carrying the Loader service.
*/
export async function apply(ctx: Context, config: Config = {}): Promise<void> {
if (ctx.loader.builtins[REPOSITORY_PLUGIN_BUILTIN] !== undefined) {
throw new Error(`Loader builtin ${REPOSITORY_PLUGIN_BUILTIN} is already registered`)
}
const repositories = (config.repositories ?? []).map(resolveRepositorySpecifier)
if (new Set(repositories).size !== repositories.length) {
throw new Error('repository sources must resolve to unique exact specifiers')
}
const cache = new RepositoryCache(resolveRepositoryCacheDirectory(config.cacheDir))
await ctx.effect(async function* () {
ctx.loader.builtins[REPOSITORY_PLUGIN_BUILTIN] = preparedRuntime
yield () => {
if (ctx.loader.builtins[REPOSITORY_PLUGIN_BUILTIN] === preparedRuntime) {
Reflect.deleteProperty(ctx.loader.builtins, REPOSITORY_PLUGIN_BUILTIN)
}
}
for (const repository of repositories) {
const plugin = await loadPreparedRepository(ctx, cache, repository)
yield plugin.dispose
}
}, 'repository-plugin runtime and sources')
}

View File

@@ -1,30 +0,0 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-repository-plugin`.
* @module @deepseek-ai/dsh-repository-plugin/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-repository-plugin'
/** Cordis companion plugin name. */
export const name = 'repository-plugin-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: the package owns no service state; Loader fibers and the existing skill
* and MCP owners expose the authoritative lifecycle relationships for its composed children.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */

View File

@@ -1,156 +0,0 @@
/**
* Parser for the common `.mcp.json` file consumed by prepared repository plugins.
* @module
*/
import { z } from 'zod'
/**
* Restates dsh-mcp-client's `SERVER_NAME_PATTERN` rather than importing it:
* the prepare bin must stay a zod-only module graph (no tools service, no MCP
* SDK). Exported so `repository-plugin.spec.ts` pins equality with the
* client's exported pattern — prepare-time validation cannot drift from the
* registry that enforces uniqueness.
*/
export const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/
const ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
const PLACEHOLDER_PATTERN = /\$\{([^}]*)\}/g
const stringMap = z.record(z.string(), z.string())
const stdioServerSchema = z.object({
type: z.literal('stdio').optional(),
command: z.string().min(1),
args: z.array(z.string()).optional(),
env: stringMap.optional(),
}).strict()
const httpServerSchema = z.object({
type: z.literal('http'),
url: z.string().min(1),
headers: stringMap.optional(),
}).strict()
const documentSchema = z.object({
mcpServers: z.record(z.string(), z.union([stdioServerSchema, httpServerSchema])),
}).strict()
/** One supported server entry from the common `.mcp.json` format. */
export type McpServerDefinition = z.infer<typeof stdioServerSchema> | z.infer<typeof httpServerSchema>
/** Parsed common MCP document before process-environment expansion. */
export interface McpDocument {
mcpServers: Record<string, McpServerDefinition>
}
/** Resolved input handed to the existing `dsh-mcp-client` Config schema. */
export type ResolvedMcpServer =
| {
transport: 'stdio'
serverName: string
command: string
args: string[]
env: Record<string, string>
cwd: string
failOnStartupError: true
}
| {
transport: 'streamable-http'
serverName: string
url: string
headers: Record<string, string>
failOnStartupError: true
}
function assertTemplate(value: string, location: string): void {
for (const match of value.matchAll(PLACEHOLDER_PATTERN)) {
const name = match[1] as string
if (!ENVIRONMENT_NAME_PATTERN.test(name)) {
throw new Error(`${location} contains an unsupported environment placeholder ${JSON.stringify(match[0])}`)
}
}
if (value.replace(PLACEHOLDER_PATTERN, '').includes('${')) {
throw new Error(`${location} contains an unterminated environment placeholder`)
}
}
function visitStrings(serverName: string, definition: McpServerDefinition, visit: (value: string, location: string) => void): void {
if ('command' in definition) {
visit(definition.command, `mcpServers.${serverName}.command`)
definition.args?.forEach((value, index) => { visit(value, `mcpServers.${serverName}.args[${index}]`) })
Object.entries(definition.env ?? {}).forEach(([name, value]) => { visit(value, `mcpServers.${serverName}.env.${name}`) })
return
}
visit(definition.url, `mcpServers.${serverName}.url`)
Object.entries(definition.headers ?? {}).forEach(([name, value]) => { visit(value, `mcpServers.${serverName}.headers.${name}`) })
}
/**
* Parse and validate one common `.mcp.json` document without resolving environment values.
* @param content - UTF-8 JSON document.
* @returns the supported stdio and Streamable HTTP server definitions.
*/
export function parseMcpDocument(content: string): McpDocument {
let value: unknown
try {
value = JSON.parse(content) as unknown
} catch (cause) {
throw new Error('invalid .mcp.json: expected JSON', { cause })
}
const result = documentSchema.safeParse(value)
if (!result.success) throw new Error(`invalid .mcp.json:\n${z.prettifyError(result.error)}`)
for (const [serverName, definition] of Object.entries(result.data.mcpServers)) {
if (!SERVER_NAME_PATTERN.test(serverName)) {
throw new Error(`invalid .mcp.json: server name ${JSON.stringify(serverName)} must match ${SERVER_NAME_PATTERN.source}`)
}
visitStrings(serverName, definition, assertTemplate)
}
return result.data
}
function expand(value: string, environment: NodeJS.ProcessEnv, location: string): string {
return value.replace(PLACEHOLDER_PATTERN, (_placeholder, name: string) => {
const replacement = environment[name]
if (replacement === undefined) throw new Error(`${location} requires missing environment variable ${name}`)
return replacement
})
}
function expandMap(values: Record<string, string> | undefined, environment: NodeJS.ProcessEnv, location: string): Record<string, string> {
return Object.fromEntries(Object.entries(values ?? {}).map(([name, value]) => [
name,
expand(value, environment, `${location}.${name}`),
]))
}
/**
* Resolve supported MCP definitions to inputs for the existing MCP client.
* @param document - validated common MCP document.
* @param environment - process environment used for exact `${NAME}` expansion.
* @param cwd - prepared plugin directory used for stdio child processes.
* @returns one existing-client config input per declared server.
*/
export function resolveMcpServers(document: McpDocument, environment: NodeJS.ProcessEnv, cwd: string): ResolvedMcpServer[] {
return Object.entries(document.mcpServers).map(([serverName, definition]) => {
if ('command' in definition) {
return {
transport: 'stdio',
serverName,
command: expand(definition.command, environment, `mcpServers.${serverName}.command`),
args: (definition.args ?? []).map((value, index) => expand(value, environment, `mcpServers.${serverName}.args[${index}]`)),
env: expandMap(definition.env, environment, `mcpServers.${serverName}.env`),
cwd,
failOnStartupError: true,
}
}
const url = expand(definition.url, environment, `mcpServers.${serverName}.url`)
const protocol = new URL(url).protocol
if (protocol !== 'http:' && protocol !== 'https:') {
throw new Error(`mcpServers.${serverName}.url must use http or https`)
}
return {
transport: 'streamable-http',
serverName,
url,
headers: expandMap(definition.headers, environment, `mcpServers.${serverName}.headers`),
failOnStartupError: true,
}
})
}

View File

@@ -1,130 +0,0 @@
/**
* GitHub repository source validation and prepared-wrapper loading.
* @module
*/
import { readFile } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import type { Context, Fiber, FiberState, Plugin } from 'cordis'
import type { RepositoryCache } from '@cordisjs/plugin-loader/repository'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
import { z } from 'zod'
import {
PREPARED_ENTRY_FILENAME,
REPOSITORY_PLUGIN_PACKAGE_NAME,
REPOSITORY_PLUGIN_PREPARE_COMMAND,
hasRepositoryPrepareCommand,
} from './format.ts'
// Value mirror: Cordis's const enum has no runtime object to import. Keep
// aligned with `packages/self-modification/tool-cordis/src/fiber-state.ts`.
const FIBER_ACTIVE = 2 as FiberState.ACTIVE
/** Directory under the Harness home containing immutable repository generations. */
export const DEFAULT_REPOSITORY_CACHE_DIRECTORY = 'repository-plugins'
// The ref segment excludes `#` so `github:o/r#a#b` fails here — at the config
// parser, with the syntax the error message promises — instead of inside the
// cache's pnpm install ('misconfiguration fails loud at the earliest
// resolvable point').
const GITHUB_SOURCE_PATTERN = /^github:([^/\s#&]+)\/([^/\s#&]+)#([^\s#&]+)(?:&path:(\/[^\s&]+))?$/
const installedPackageSchema = z.looseObject({
devDependencies: z.looseObject({
[REPOSITORY_PLUGIN_PACKAGE_NAME]: z.string().min(1),
}),
scripts: z.looseObject({
prepack: z.string().min(1).refine(
hasRepositoryPrepareCommand,
{ message: `must invoke ${REPOSITORY_PLUGIN_PREPARE_COMMAND}` },
),
}),
})
function validPluginPath(path: string): boolean {
const segments = path.split('/').slice(1)
return segments.length > 0
&& segments.at(-1) === '.dsh-plugin'
&& segments.every(segment => segment.length > 0 && segment !== '.' && segment !== '..')
}
/**
* Normalize one user-facing GitHub source to the exact pnpm dependency specifier.
* @param configured - `github:owner/repo#ref` with an optional `&path:/.../.dsh-plugin`.
* @returns the exact specifier, with the root `.dsh-plugin` subpath added when omitted.
* @throws when the GitHub owner, repository, explicit ref, or plugin subpath is invalid.
*/
export function resolveRepositorySpecifier(configured: string): string {
const match = GITHUB_SOURCE_PATTERN.exec(configured)
if (match === null) {
throw new Error(`repository source must use github:owner/repo#<ref> with an optional &path:/.../.dsh-plugin: ${JSON.stringify(configured)}`)
}
const path = match[4]
if (path !== undefined && !validPluginPath(path)) {
throw new Error(`repository source path must be an absolute repository subpath ending in .dsh-plugin without empty, . or .. segments: ${JSON.stringify(path)}`)
}
return path === undefined ? `${configured}&path:/.dsh-plugin` : configured
}
/**
* Resolve the persistent repository cache root.
* @param configured - explicit cache directory, or undefined for `$DSH_HOME/cache/repository-plugins`.
* @returns an absolute cache directory.
*/
export function resolveRepositoryCacheDirectory(configured: string | undefined): string {
return resolve(configured ?? join(resolveDshHome(), 'cache', DEFAULT_REPOSITORY_CACHE_DIRECTORY))
}
async function assertInstalledPackageMetadata(directory: string): Promise<void> {
let value: unknown
try {
value = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8')) as unknown
} catch (cause) {
throw new Error(`failed to read installed DSH plugin package metadata in ${directory}`, { cause })
}
const result = installedPackageSchema.safeParse(value)
if (!result.success) {
throw new Error([
`installed DSH plugin package must declare a non-empty scripts.prepack that invokes ${JSON.stringify(REPOSITORY_PLUGIN_PREPARE_COMMAND)}, and declare ${JSON.stringify(REPOSITORY_PLUGIN_PACKAGE_NAME)} in devDependencies:`,
z.prettifyError(result.error),
'Clear the matching repository cache generation before retrying the same source, or select a different exact source/ref/path after fixing the package.',
].join('\n'))
}
}
/**
* Load one exact repository generation's generated wrapper as a child Cordis fiber.
* @param ctx - repository runtime context that owns the child.
* @param cache - package-manager-native immutable repository cache.
* @param specifier - normalized exact pnpm dependency specifier.
* @returns the settled prepared-wrapper fiber.
* @throws when installation, wrapper import, manifest validation, or child registration fails.
*/
export async function loadPreparedRepository(
ctx: Context,
cache: Pick<RepositoryCache, 'resolve'>,
specifier: string,
): Promise<Fiber> {
const directory = await cache.resolve(specifier)
const filename = join(directory, PREPARED_ENTRY_FILENAME)
try {
await assertInstalledPackageMetadata(directory)
const plugin = await import(/* @vite-ignore */pathToFileURL(filename).href) as Plugin
const fiber = ctx.plugin(plugin)
await fiber
// Awaiting a service-gated fiber returns while it is still PENDING (the
// generated wrapper injects `skills`/`tools` per its manifest). This
// runtime commits the repository configuration transactionally, so a
// composition that never provides a required service must reject the
// transaction here — not settle ACTIVE with a silently pending child.
if (fiber.state !== FIBER_ACTIVE) {
const missing = Object.keys(fiber.inject).filter(service => fiber.ctx.get(service) === undefined)
/* v8 ignore next 2 -- the 'unknown' arm needs a service to appear after the state read; not deterministically stageable. */
const detail = missing.join(', ') || 'unknown'
throw new Error(`prepared wrapper did not activate (waiting for services: ${detail})`)
}
return await fiber
} catch (cause) {
throw new Error(`failed to load prepared repository Plugin ${JSON.stringify(specifier)} from ${filename}`, { cause })
}
}

View File

@@ -1,121 +0,0 @@
import { describe, expect, it } from 'vitest'
import { SERVER_NAME_PATTERN as CLIENT_SERVER_NAME_PATTERN } from '@deepseek-ai/dsh-mcp-client'
import { SERVER_NAME_PATTERN, parseMcpDocument, resolveMcpServers } from '../src/mcp.ts'
describe('repository plugin common .mcp.json support', () => {
it('validates server names with exactly the pattern the MCP client registry enforces', () => {
// mcp.ts restates the pattern to keep the prepare bin's module graph
// zod-only; this pin is the drift guard.
expect(SERVER_NAME_PATTERN.source).toBe(CLIENT_SERVER_NAME_PATTERN.source)
expect(SERVER_NAME_PATTERN.flags).toBe(CLIENT_SERVER_NAME_PATTERN.flags)
})
it('maps Expo-style HTTP servers to the existing Streamable HTTP client config', () => {
const document = parseMcpDocument(JSON.stringify({
mcpServers: {
expo: { type: 'http', url: 'https://mcp.expo.dev/mcp' },
},
}))
expect(resolveMcpServers(document, {}, '/plugin')).toEqual([{
transport: 'streamable-http',
serverName: 'expo',
url: 'https://mcp.expo.dev/mcp',
headers: {},
failOnStartupError: true,
}])
})
it('maps DataJunction-style stdio servers and expands exact environment placeholders', () => {
const document = parseMcpDocument(JSON.stringify({
mcpServers: {
datajunction: {
command: 'dj-mcp',
args: ['--endpoint', '${DJ_API_URL}'],
env: { DJ_API_URL: '${DJ_API_URL}' },
},
},
}))
expect(resolveMcpServers(document, { DJ_API_URL: 'http://localhost:8000' }, '/plugin')).toEqual([{
transport: 'stdio',
serverName: 'datajunction',
command: 'dj-mcp',
args: ['--endpoint', 'http://localhost:8000'],
env: { DJ_API_URL: 'http://localhost:8000' },
cwd: '/plugin',
failOnStartupError: true,
}])
})
it('fails loud when a declared environment value is absent', () => {
const document = parseMcpDocument(JSON.stringify({
mcpServers: { datajunction: { command: 'dj-mcp', env: { DJ_API_URL: '${DJ_API_URL}' } } },
}))
expect(() => resolveMcpServers(document, {}, '/plugin')).toThrow('missing environment variable DJ_API_URL')
})
it('accepts explicit stdio defaults and expands HTTP URLs and headers', () => {
const document = parseMcpDocument(JSON.stringify({
mcpServers: {
local: { type: 'stdio', command: 'local-mcp' },
remote: {
type: 'http',
url: 'http://${MCP_HOST}/mcp',
headers: { Authorization: 'Bearer ${MCP_TOKEN}' },
},
},
}))
expect(resolveMcpServers(document, { MCP_HOST: 'localhost:3000', MCP_TOKEN: 'test-token' }, '/plugin')).toEqual([
{
transport: 'stdio',
serverName: 'local',
command: 'local-mcp',
args: [],
env: {},
cwd: '/plugin',
failOnStartupError: true,
},
{
transport: 'streamable-http',
serverName: 'remote',
url: 'http://localhost:3000/mcp',
headers: { Authorization: 'Bearer test-token' },
failOnStartupError: true,
},
])
})
it('rejects malformed JSON, server names, placeholders, and non-HTTP URLs', () => {
expect(() => parseMcpDocument('{')).toThrow('expected JSON')
expect(() => parseMcpDocument(JSON.stringify({
mcpServers: { 'bad name': { command: 'server' } },
}))).toThrow('server name')
expect(() => parseMcpDocument(JSON.stringify({
mcpServers: { bad: { command: '${BAD-NAME}' } },
}))).toThrow('unsupported environment placeholder')
expect(() => parseMcpDocument(JSON.stringify({
mcpServers: { bad: { command: '${UNFINISHED' } },
}))).toThrow('unterminated environment placeholder')
const ftp = parseMcpDocument(JSON.stringify({
mcpServers: { remote: { type: 'http', url: 'ftp://example.test/mcp' } },
}))
expect(() => resolveMcpServers(ftp, {}, '/plugin')).toThrow('must use http or https')
})
it('rejects Work IQ OAuth fields instead of treating them as unauthenticated HTTP', () => {
expect(() => parseMcpDocument(JSON.stringify({
mcpServers: {
workiq: {
type: 'http',
url: 'https://workiq.microsoft.com/mcp',
oauthClientId: 'client-id',
oauthPublicClient: true,
auth: { redirectPort: 3317 },
},
},
}))).toThrow('invalid .mcp.json')
})
})

View File

@@ -1,676 +0,0 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, relative, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { RepositoryCache } from '@cordisjs/plugin-loader/repository'
import SkillService from '@deepseek-ai/dsh-skill'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import InvariantService from '@deepseek-ai/dsh-invariants'
import * as RepositoryPlugin from '@deepseek-ai/dsh-repository-plugin'
import * as RepositoryPluginInvariant from '@deepseek-ai/dsh-repository-plugin/invariant'
import { parsePreparedPluginConfig } from '../src/format.ts'
import {
loadPreparedRepository,
resolveRepositoryCacheDirectory,
resolveRepositorySpecifier,
} from '../src/source.ts'
const roots: string[] = []
async function temporaryDirectory(name: string): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), `dsh-repository-plugin-${name}-`))
roots.push(directory)
return directory
}
async function writePlugin(
root: string,
name: string,
dsh: Record<string, unknown>,
prepack = RepositoryPlugin.REPOSITORY_PLUGIN_PREPARE_COMMAND,
devDependencies: Record<string, string> = {
[RepositoryPlugin.REPOSITORY_PLUGIN_PACKAGE_NAME]: '0.0.1',
},
): Promise<string> {
const directory = join(root, '.dsh-plugin')
await mkdir(directory, { recursive: true })
await writeFile(join(directory, 'package.json'), `${JSON.stringify({
name,
version: '0.0.0',
devDependencies,
scripts: { prepack },
dsh,
}, undefined, 2)}\n`)
return directory
}
async function writeSkill(root: string, name: string): Promise<void> {
const directory = join(root, name)
await mkdir(directory, { recursive: true })
await writeFile(join(directory, 'SKILL.md'), `---\nname: ${name}\ndescription: Repository fixture skill.\n---\n\nStatic instructions.\n`)
}
afterEach(async () => {
vi.restoreAllMocks()
vi.unstubAllEnvs()
await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true })))
})
describe('dsh-plugin-prepare', () => {
it('copies declared static assets and emits the fixed import-free wrapper', async () => {
const root = await temporaryDirectory('prepare')
await writeSkill(join(root, 'skills'), 'repository-fixture')
await writeFile(join(root, '.mcp.json'), JSON.stringify({
mcpServers: {
expo: { type: 'http', url: 'https://mcp.expo.dev/mcp' },
},
}))
const directory = await writePlugin(root, 'fixture-plugin', {
skills: ['../skills'],
mcpServers: '../.mcp.json',
})
await expect(RepositoryPlugin.prepareDshPlugin(directory)).resolves.toEqual({
name: 'fixture-plugin',
skills: ['dsh-plugin-assets/skills/0'],
mcpServers: 'dsh-plugin-assets/.mcp.json',
})
const wrapper = await readFile(join(directory, RepositoryPlugin.PREPARED_ENTRY_FILENAME), 'utf8')
expect(wrapper).toContain(`ctx.loader.builtins["${RepositoryPlugin.REPOSITORY_PLUGIN_BUILTIN}"]`)
// Import-free means no static AND no dynamic imports; `import.meta.url`
// (no whitespace, no call parenthesis) is the one allowed appearance.
expect(wrapper).not.toMatch(/\b(?:import|from)\s|\bimport\s*\(/)
await expect(readFile(join(directory, 'dsh-plugin-assets/skills/0/repository-fixture/SKILL.md'), 'utf8'))
.resolves.toContain('Static instructions.')
await expect(readFile(join(directory, 'dsh-plugin-assets/.mcp.json'), 'utf8'))
.resolves.toContain('mcp.expo.dev')
})
it('preserves a compiled package entry and accepts a build before the package prepare command', async () => {
const root = await temporaryDirectory('compiled-entry')
const directory = await writePlugin(root, 'compiled-entry-fixture', {
entry: './lib/plugin.mjs',
}, 'npm run build && dsh-plugin-prepare')
await mkdir(join(directory, 'lib'))
await writeFile(join(directory, 'lib/plugin.mjs'), 'export default { name: "compiled-entry" }\n')
await expect(RepositoryPlugin.prepareDshPlugin(directory)).resolves.toEqual({
name: 'compiled-entry-fixture',
skills: [],
entry: './lib/plugin.mjs',
})
const wrapper = await readFile(join(directory, RepositoryPlugin.PREPARED_ENTRY_FILENAME), 'utf8')
expect(wrapper).toContain('await import(manifest.entry)')
expect(wrapper).toContain('"entry":"./lib/plugin.mjs"')
})
it('rejects unsupported OAuth MCP metadata before publishing outputs', async () => {
const root = await temporaryDirectory('oauth')
await writeFile(join(root, '.mcp.json'), JSON.stringify({
mcpServers: {
workiq: {
type: 'http',
url: 'https://workiq.microsoft.com/mcp',
oauthClientId: 'client-id',
oauthPublicClient: true,
auth: { redirectPort: 3317 },
},
},
}))
const directory = await writePlugin(root, 'unsupported-oauth', { mcpServers: '../.mcp.json' })
await expect(RepositoryPlugin.prepareDshPlugin(directory)).rejects.toThrow('invalid .mcp.json')
await expect(readFile(join(directory, RepositoryPlugin.PREPARED_ENTRY_FILENAME), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
})
it('rejects invalid metadata, missing assets, wrong asset types, and escaped paths', async () => {
const malformedRoot = await temporaryDirectory('malformed-package')
const malformed = join(malformedRoot, '.dsh-plugin')
await mkdir(malformed)
await writeFile(join(malformed, 'package.json'), '{')
await expect(RepositoryPlugin.prepareDshPlugin(malformed)).rejects.toThrow('failed to read DSH plugin package metadata')
const lifecycleRoot = await temporaryDirectory('wrong-lifecycle')
const lifecycle = join(lifecycleRoot, '.dsh-plugin')
await mkdir(lifecycle)
await writeFile(join(lifecycle, 'package.json'), JSON.stringify({
name: 'wrong-lifecycle',
scripts: { prepare: 'dsh-plugin-prepare' },
dsh: { skills: ['../skills'] },
}))
await expect(RepositoryPlugin.prepareDshPlugin(lifecycle)).rejects.toThrow('prepack')
const skippedPrepareRoot = await temporaryDirectory('skipped-prepare')
const skippedPrepare = await writePlugin(
skippedPrepareRoot,
'skipped-prepare',
{ skills: ['../skills'] },
'npm run build',
)
await expect(RepositoryPlugin.prepareDshPlugin(skippedPrepare)).rejects.toThrow('must invoke dsh-plugin-prepare')
const undeclaredPrepareRoot = await temporaryDirectory('undeclared-prepare-dependency')
const undeclaredPrepare = await writePlugin(
undeclaredPrepareRoot,
'undeclared-prepare-dependency',
{ skills: ['../skills'] },
RepositoryPlugin.REPOSITORY_PLUGIN_PREPARE_COMMAND,
{},
)
await expect(RepositoryPlugin.prepareDshPlugin(undeclaredPrepare))
.rejects.toThrow(RepositoryPlugin.REPOSITORY_PLUGIN_PACKAGE_NAME)
const emptyRoot = await temporaryDirectory('empty-metadata')
const empty = await writePlugin(emptyRoot, 'empty', {})
await expect(RepositoryPlugin.prepareDshPlugin(empty)).rejects.toThrow('declare at least one skill root, mcpServers file, or compiled entry')
const missingRoot = await temporaryDirectory('missing-asset')
const missing = await writePlugin(missingRoot, 'missing', { skills: ['../missing'] })
await expect(RepositoryPlugin.prepareDshPlugin(missing)).rejects.toThrow('asset does not exist')
const absoluteRoot = await temporaryDirectory('absolute-asset')
const absolute = await writePlugin(absoluteRoot, 'absolute', { skills: [absoluteRoot] })
await expect(RepositoryPlugin.prepareDshPlugin(absolute)).rejects.toThrow('asset path must be relative')
const wrongTypeRoot = await temporaryDirectory('wrong-type')
await writeFile(join(wrongTypeRoot, 'not-a-directory'), 'text')
const wrongType = await writePlugin(wrongTypeRoot, 'wrong-type', { skills: ['../not-a-directory'] })
await expect(RepositoryPlugin.prepareDshPlugin(wrongType)).rejects.toThrow('asset is not a directory')
const wrongMcpRoot = await temporaryDirectory('wrong-mcp-type')
await mkdir(join(wrongMcpRoot, 'not-a-file'))
const wrongMcp = await writePlugin(wrongMcpRoot, 'wrong-mcp', { mcpServers: '../not-a-file' })
await expect(RepositoryPlugin.prepareDshPlugin(wrongMcp)).rejects.toThrow('asset is not a file')
const containingRoot = await temporaryDirectory('containing-root')
const containing = await writePlugin(containingRoot, 'containing', { skills: ['..'] })
await expect(RepositoryPlugin.prepareDshPlugin(containing)).rejects.toThrow('cannot contain the .dsh-plugin package')
const escapedRoot = await temporaryDirectory('escaped-root')
const outside = await temporaryDirectory('outside-root')
await writeSkill(outside, 'outside-skill')
const escaped = await writePlugin(escapedRoot, 'escaped', { skills: [relative(join(escapedRoot, '.dsh-plugin'), outside)] })
await expect(RepositoryPlugin.prepareDshPlugin(escaped)).rejects.toThrow('escapes its plugin source root')
const escapedEntryRoot = await temporaryDirectory('escaped-entry')
await writeFile(join(escapedEntryRoot, 'outside.mjs'), 'export default {}\n')
const escapedEntry = await writePlugin(escapedEntryRoot, 'escaped-entry', { entry: '../outside.mjs' })
await expect(RepositoryPlugin.prepareDshPlugin(escapedEntry)).rejects.toThrow('escapes its plugin source root')
})
it('validates prepared wrapper configs with optional MCP assets and code entries', () => {
expect(() => parsePreparedPluginConfig({})).toThrow('invalid prepared DSH plugin')
expect(parsePreparedPluginConfig({
baseUrl: 'file:///plugin/dsh-plugin.mjs',
manifest: { name: 'fixture', skills: [], mcpServers: 'dsh-plugin-assets/.mcp.json', entry: './lib/plugin.js' },
})).toEqual({
baseUrl: 'file:///plugin/dsh-plugin.mjs',
manifest: { name: 'fixture', skills: [], mcpServers: 'dsh-plugin-assets/.mcp.json', entry: './lib/plugin.js' },
})
})
})
describe('prepared repository plugin Loader composition', () => {
it('mounts and removes copied skills through the real Loader and skill-local provider', async () => {
const root = await temporaryDirectory('loader')
await writeSkill(join(root, 'skills'), 'loaded-from-repository')
const directory = await writePlugin(root, 'loader-fixture', { skills: ['../skills'] })
await RepositoryPlugin.prepareDshPlugin(directory)
const ctx = new Context()
ctx.baseUrl = pathToFileURL(directory).href + '/'
await ctx.plugin(Loader)
await ctx.plugin(SkillService)
const registrar = ctx.plugin(RepositoryPlugin)
await registrar
expect(ctx.loader.builtins[RepositoryPlugin.REPOSITORY_PLUGIN_BUILTIN]).toBeDefined()
const id = await ctx.loader.create({
name: pathToFileURL(join(directory, RepositoryPlugin.PREPARED_ENTRY_FILENAME)).href,
})
await ctx.loader.await()
await expect(ctx.skills.get('loaded-from-repository')).resolves.toMatchObject({
name: 'loaded-from-repository',
provider: 'repository:loader-fixture',
content: 'Static instructions.',
})
await ctx.loader.remove(id)
await expect(ctx.skills.get('loaded-from-repository')).resolves.toBeUndefined()
await registrar.dispose()
expect(ctx.loader.builtins[RepositoryPlugin.REPOSITORY_PLUGIN_BUILTIN]).toBeUndefined()
await ctx.fiber.dispose()
})
it('mounts and removes the repository package code entry through the real Loader', async () => {
const root = await temporaryDirectory('code-loader')
const directory = await writePlugin(root, 'code-loader-fixture', { entry: './lib/plugin.mjs' })
await mkdir(join(directory, 'lib'))
await writeFile(join(directory, 'lib/plugin.mjs'), [
"export const name = 'repository-code-proof'",
'export function apply(ctx) {',
" ctx.provide('repositoryCodeProof', { source: 'compiled-entry' })",
'}',
'',
].join('\n'))
await RepositoryPlugin.prepareDshPlugin(directory)
const ctx = new Context()
ctx.baseUrl = pathToFileURL(directory).href + '/'
await ctx.plugin(Loader)
await ctx.plugin(RepositoryPlugin)
const id = await ctx.loader.create({
name: pathToFileURL(join(directory, RepositoryPlugin.PREPARED_ENTRY_FILENAME)).href,
})
await ctx.loader.await()
const getService = (name: string): unknown => (ctx as unknown as { get(name: string): unknown }).get(name)
expect(getService('repositoryCodeProof')).toEqual({ source: 'compiled-entry' })
await ctx.loader.remove(id)
expect(getService('repositoryCodeProof')).toBeUndefined()
await ctx.fiber.dispose()
})
it('mounts and removes tools discovered from a repository MCP server', async () => {
const root = await temporaryDirectory('mcp-loader-success')
const server = join(root, 'mcp-server.mjs')
await writeFile(server, [
"import { createInterface } from 'node:readline'",
'const lines = createInterface({ input: process.stdin })',
'for await (const line of lines) {',
' const request = JSON.parse(line)',
" if (!('id' in request)) continue",
' let result',
" if (request.method === 'initialize') {",
' result = {',
' protocolVersion: request.params.protocolVersion,',
' capabilities: { tools: {} },',
" serverInfo: { name: 'repository-fixture', version: '0.0.0' },",
' }',
" } else if (request.method === 'tools/list') {",
' result = {',
' tools: [{',
" name: 'proof',",
" description: 'Repository MCP proof.',",
" inputSchema: { type: 'object', properties: {} },",
' }],',
' }',
' } else {',
' result = {}',
' }',
" process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: request.id, result })}\\n`)",
'}',
'',
].join('\n'))
await writeFile(join(root, '.mcp.json'), JSON.stringify({
mcpServers: { online: { command: process.execPath, args: [server] } },
}))
const directory = await writePlugin(root, 'mcp-loader-success-fixture', { mcpServers: '../.mcp.json' })
await RepositoryPlugin.prepareDshPlugin(directory)
const ctx = new Context()
ctx.baseUrl = pathToFileURL(directory).href + '/'
await ctx.plugin(Loader)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(RepositoryPlugin)
const id = await ctx.loader.create({
name: pathToFileURL(join(directory, RepositoryPlugin.PREPARED_ENTRY_FILENAME)).href,
})
await ctx.loader.await()
expect(ctx.tools.get('mcp__online__proof')).toBeDefined()
await ctx.loader.remove(id)
expect(ctx.tools.get('mcp__online__proof')).toBeUndefined()
await ctx.fiber.dispose()
})
it('fails an MCP repository plugin load when its declared server cannot connect', async () => {
const root = await temporaryDirectory('mcp-loader')
await writeFile(join(root, '.mcp.json'), JSON.stringify({
mcpServers: { offline: { command: join(root, 'missing-mcp-command') } },
}))
const directory = await writePlugin(root, 'mcp-loader-fixture', { mcpServers: '../.mcp.json' })
await RepositoryPlugin.prepareDshPlugin(directory)
const ctx = new Context()
ctx.baseUrl = pathToFileURL(directory).href + '/'
await ctx.plugin(Loader)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(RepositoryPlugin)
await expect(ctx.loader.create({
name: pathToFileURL(join(directory, RepositoryPlugin.PREPARED_ENTRY_FILENAME)).href,
})).rejects.toThrow('initial connection or tool synchronization failed')
expect(ctx.tools.schemas().some(tool => tool.name.startsWith('mcp__offline__'))).toBe(false)
await ctx.fiber.dispose()
})
it('rejects hostile prepared paths before mounting children', async () => {
const root = await temporaryDirectory('prepared-paths')
const ctx = new Context()
ctx.baseUrl = pathToFileURL(root).href + '/'
await ctx.plugin(Loader)
await ctx.plugin(RepositoryPlugin)
for (const [filename, skillPath] of [
['absolute.mjs', resolve(root)],
['escaped.mjs', '../outside'],
] as const) {
const wrapper = join(root, filename)
await writeFile(wrapper, [
"export const inject = ['loader']",
'export async function apply(ctx) {',
` await ctx.plugin(ctx.loader.builtins['${RepositoryPlugin.REPOSITORY_PLUGIN_BUILTIN}'], {`,
` baseUrl: import.meta.url, manifest: { name: 'hostile', skills: [${JSON.stringify(skillPath)}] },`,
' })',
'}',
'',
].join('\n'))
await expect(ctx.loader.create({ name: pathToFileURL(wrapper).href })).rejects.toThrow('prepared DSH plugin path')
}
await ctx.fiber.dispose()
})
it('fails the plugin load when a declared skill root is missing or not a directory', async () => {
const root = await temporaryDirectory('missing-skill-root')
await writeFile(join(root, 'not-a-directory'), 'text')
const ctx = new Context()
ctx.baseUrl = pathToFileURL(root).href + '/'
await ctx.plugin(Loader)
await ctx.plugin(SkillService)
await ctx.plugin(RepositoryPlugin)
for (const [filename, skillPath, message] of [
['missing.mjs', 'dsh-plugin-assets/skills/0', 'skill root is missing from the installed package'],
['file.mjs', 'not-a-directory', 'skill root is not a directory'],
] as const) {
const wrapper = join(root, filename)
await writeFile(wrapper, [
"export const inject = ['loader']",
'export async function apply(ctx) {',
` await ctx.plugin(ctx.loader.builtins['${RepositoryPlugin.REPOSITORY_PLUGIN_BUILTIN}'], {`,
` baseUrl: import.meta.url, manifest: { name: 'damaged', skills: [${JSON.stringify(skillPath)}] },`,
' })',
'}',
'',
].join('\n'))
await expect(ctx.loader.create({ name: pathToFileURL(wrapper).href })).rejects.toThrow(message)
}
await ctx.fiber.dispose()
})
it('rejects duplicate builtin ownership and preserves a later replacement on teardown', async () => {
const ctx = new Context()
await ctx.plugin(Loader)
const registrar = ctx.plugin(RepositoryPlugin)
await registrar
await expect(RepositoryPlugin.apply(ctx)).rejects.toThrow('already registered')
const replacement = { name: 'replacement', apply() {} }
ctx.loader.builtins[RepositoryPlugin.REPOSITORY_PLUGIN_BUILTIN] = replacement
await registrar.dispose()
expect(ctx.loader.builtins[RepositoryPlugin.REPOSITORY_PLUGIN_BUILTIN]).toBe(replacement)
await ctx.fiber.dispose()
})
})
describe('configured GitHub repository sources', () => {
it('defaults an omitted source list and rejects unknown configuration fields', () => {
expect(RepositoryPlugin.Config.parse(undefined)).toEqual({ repositories: [] })
expect(RepositoryPlugin.Config.safeParse({ repositories: [], unexpected: true }).success).toBe(false)
})
it('accepts an empty direct-apply config', async () => {
const ctx = new Context()
await ctx.plugin(Loader)
await RepositoryPlugin.apply(ctx, {})
expect(ctx.loader.builtins[RepositoryPlugin.REPOSITORY_PLUGIN_BUILTIN]).toBeDefined()
await ctx.fiber.dispose()
})
it('adds the root plugin subpath and preserves an explicit nested plugin subpath', () => {
expect(resolveRepositorySpecifier('github:PolyArch/humanize#v1.0.0'))
.toBe('github:PolyArch/humanize#v1.0.0&path:/.dsh-plugin')
expect(resolveRepositorySpecifier('github:owner/repository#feature/ref&path:/plugins/one/.dsh-plugin'))
.toBe('github:owner/repository#feature/ref&path:/plugins/one/.dsh-plugin')
})
it('rejects absent refs and invalid plugin subpaths', () => {
for (const source of [
'github:owner/repository',
'github:owner/repository#',
'github:owner/repository#a#b',
'https://github.com/owner/repository#ref',
'github:owner/repository#ref&path:relative/.dsh-plugin',
]) {
expect(() => resolveRepositorySpecifier(source)).toThrow('must use github:owner/repo#<ref>')
}
for (const path of [
'/plugins//.dsh-plugin',
'/plugins/../.dsh-plugin',
'/plugins/./.dsh-plugin',
'/plugins/not-a-plugin',
]) {
expect(() => resolveRepositorySpecifier(`github:owner/repository#ref&path:${path}`))
.toThrow('path must be an absolute repository subpath')
}
})
it('resolves the default cache under DSH_HOME and an explicit cache absolutely', async () => {
const root = await temporaryDirectory('cache-root')
vi.stubEnv('DSH_HOME', root)
expect(resolveRepositoryCacheDirectory(undefined)).toBe(join(root, 'cache', 'repository-plugins'))
expect(resolveRepositoryCacheDirectory(join(root, 'explicit'))).toBe(join(root, 'explicit'))
})
it('loads a configured source through the immutable cache and removes its skill on teardown', async () => {
const root = await temporaryDirectory('configured-source')
await writeSkill(join(root, 'skills'), 'configured-repository-skill')
const directory = await writePlugin(root, 'configured-source-fixture', { skills: ['../skills'] })
await RepositoryPlugin.prepareDshPlugin(directory)
const resolved: string[] = []
const cacheDirectory = join(root, 'cache')
vi.spyOn(RepositoryCache.prototype, 'resolve').mockImplementation(async function (this: RepositoryCache, specifier) {
expect(this.directory).toBe(cacheDirectory)
resolved.push(specifier)
return directory
})
const ctx = new Context()
await ctx.plugin(Loader)
await ctx.plugin(SkillService)
const registrar = ctx.plugin(RepositoryPlugin, {
repositories: ['github:owner/repository#fixed-ref'],
cacheDir: cacheDirectory,
})
await registrar
expect(resolved).toEqual(['github:owner/repository#fixed-ref&path:/.dsh-plugin'])
await expect(ctx.skills.get('configured-repository-skill')).resolves.toMatchObject({
provider: 'repository:configured-source-fixture',
})
await registrar.dispose()
await expect(ctx.skills.get('configured-repository-skill')).resolves.toBeUndefined()
await ctx.fiber.dispose()
})
it('swaps generations on a live source-list update and rolls a failed candidate back', async () => {
// The headline flow: a personal-config edit reaches this plugin as a
// Loader entry.update, which restarts the row's fiber (old cleanup, then
// new apply — so the 'already registered' builtin guard must not fire).
const roots: Record<string, string> = {}
for (const generation of ['one', 'two'] as const) {
const root = await temporaryDirectory(`live-${generation}`)
await writeSkill(join(root, 'skills'), `live-skill-${generation}`)
const directory = await writePlugin(root, `live-fixture-${generation}`, { skills: ['../skills'] })
await RepositoryPlugin.prepareDshPlugin(directory)
roots[`github:owner/repository#${generation}&path:/.dsh-plugin`] = directory
}
vi.spyOn(RepositoryCache.prototype, 'resolve').mockImplementation(async (specifier) => {
const directory = roots[specifier]
if (directory === undefined) throw new Error(`unprepared generation ${specifier}`)
return directory
})
// Route the row through the Loader builtin table exactly as a config tree
// would; the module itself is the row's plugin.
const ctx2 = new Context()
await ctx2.plugin(Loader)
await ctx2.plugin(SkillService)
ctx2.loader.builtins['repository-plugins'] = RepositoryPlugin
const entryId = await ctx2.loader.create({
name: 'cordis:repository-plugins',
config: { repositories: ['github:owner/repository#one'] },
})
await ctx2.loader.await()
await expect(ctx2.skills.get('live-skill-one')).resolves.toMatchObject({ provider: 'repository:live-fixture-one' })
const entry = ctx2.loader.resolve(entryId)
await entry.update({ config: { repositories: ['github:owner/repository#two'] } })
await ctx2.loader.await()
await expect(ctx2.skills.get('live-skill-one')).resolves.toBeUndefined()
await expect(ctx2.skills.get('live-skill-two')).resolves.toMatchObject({ provider: 'repository:live-fixture-two' })
// A failed candidate (unprepared source) rejects the update and the
// transactional Loader restores the previous generation.
await expect(entry.update({ config: { repositories: ['github:owner/repository#missing'] } }))
.rejects.toThrow('unprepared generation')
await ctx2.loader.await()
await expect(ctx2.skills.get('live-skill-two')).resolves.toMatchObject({ provider: 'repository:live-fixture-two' })
await ctx2.fiber.dispose()
})
it('rejects duplicate generations and cleans the builtin after cache preparation fails', async () => {
const ctx = new Context()
await ctx.plugin(Loader)
await expect(RepositoryPlugin.apply(ctx, {
repositories: [
'github:owner/repository#ref',
'github:owner/repository#ref',
],
})).rejects.toThrow('must resolve to unique exact specifiers')
vi.spyOn(RepositoryCache.prototype, 'resolve').mockRejectedValue(new Error('prepare failed'))
await expect(RepositoryPlugin.apply(ctx, {
repositories: ['github:owner/repository#other'],
})).rejects.toThrow('prepare failed')
expect(ctx.loader.builtins[RepositoryPlugin.REPOSITORY_PLUGIN_BUILTIN]).toBeUndefined()
await ctx.fiber.dispose()
})
it('rejects a wrapper left pending by a composition without its required services', async () => {
// A skills-declaring generation mounted where no skills service exists:
// the wrapper fiber stays PENDING, and the transaction must fail loud
// instead of committing an ACTIVE row over a silently inert child.
const root = await temporaryDirectory('pending-services')
await writeSkill(join(root, 'skills'), 'pending-service-skill')
const directory = await writePlugin(root, 'pending-service-fixture', { skills: ['../skills'] })
await RepositoryPlugin.prepareDshPlugin(directory)
const ctx = new Context()
await ctx.plugin(Loader)
// Deliberately NO SkillService.
await expect(loadPreparedRepository(ctx, { resolve: async () => directory }, 'github:owner/repository#pending&path:/.dsh-plugin'))
.rejects.toMatchObject({
message: expect.stringContaining('failed to load prepared repository Plugin') as string,
cause: expect.objectContaining({
message: expect.stringContaining('waiting for services: skills') as string,
}) as Error,
})
await ctx.fiber.dispose()
})
it('labels a missing prepared wrapper with its exact source and path', async () => {
const root = await temporaryDirectory('missing-wrapper')
const directory = await writePlugin(root, 'missing-wrapper', { skills: ['../skills'] })
const ctx = new Context()
const specifier = 'github:owner/repository#missing&path:/.dsh-plugin'
await expect(loadPreparedRepository(ctx, { resolve: async () => directory }, specifier))
.rejects.toThrow(`failed to load prepared repository Plugin ${JSON.stringify(specifier)}`)
await ctx.fiber.dispose()
})
it('rejects installed source with the obsolete prepare lifecycle', async () => {
const root = await temporaryDirectory('installed-lifecycle')
await writeFile(join(root, 'package.json'), JSON.stringify({
name: 'installed-lifecycle',
devDependencies: { [RepositoryPlugin.REPOSITORY_PLUGIN_PACKAGE_NAME]: '0.0.1' },
scripts: { prepare: 'dsh-plugin-prepare' },
}))
const ctx = new Context()
await expect(loadPreparedRepository(ctx, { resolve: async () => root }, 'github:owner/repository#old&path:/.dsh-plugin'))
.rejects.toMatchObject({
cause: expect.objectContaining({
message: expect.stringContaining('must declare a non-empty scripts.prepack') as string,
}) as Error,
})
await expect(loadPreparedRepository(ctx, { resolve: async () => root }, 'github:owner/repository#old&path:/.dsh-plugin'))
.rejects.toMatchObject({
cause: expect.objectContaining({
message: expect.stringContaining('Clear the matching repository cache generation') as string,
}) as Error,
})
await ctx.fiber.dispose()
})
it('rejects an installed source whose prepack omits the package prepare command', async () => {
const root = await temporaryDirectory('installed-skipped-prepare')
await writeFile(join(root, 'package.json'), JSON.stringify({
name: 'installed-skipped-prepare',
devDependencies: { [RepositoryPlugin.REPOSITORY_PLUGIN_PACKAGE_NAME]: '0.0.1' },
scripts: { prepack: 'npm run build' },
}))
const ctx = new Context()
await expect(loadPreparedRepository(ctx, { resolve: async () => root }, 'github:owner/repository#unprepared&path:/.dsh-plugin'))
.rejects.toMatchObject({
cause: expect.objectContaining({
message: expect.stringContaining('must invoke dsh-plugin-prepare') as string,
}) as Error,
})
await ctx.fiber.dispose()
})
it('rejects installed source without the declared prepare dependency', async () => {
const root = await temporaryDirectory('installed-missing-prepare-dependency')
await writeFile(join(root, 'package.json'), JSON.stringify({
name: 'installed-missing-prepare-dependency',
scripts: { prepack: 'dsh-plugin-prepare' },
}))
const ctx = new Context()
await expect(loadPreparedRepository(ctx, { resolve: async () => root }, 'github:owner/repository#ambient-helper&path:/.dsh-plugin'))
.rejects.toMatchObject({
cause: expect.objectContaining({
message: expect.stringContaining(`${JSON.stringify(RepositoryPlugin.REPOSITORY_PLUGIN_PACKAGE_NAME)} in devDependencies`) as string,
}) as Error,
})
await ctx.fiber.dispose()
})
it('labels missing installed package metadata with its source', async () => {
const root = await temporaryDirectory('missing-installed-metadata')
const ctx = new Context()
const specifier = 'github:owner/repository#damaged&path:/.dsh-plugin'
await expect(loadPreparedRepository(ctx, { resolve: async () => root }, specifier))
.rejects.toMatchObject({
message: expect.stringContaining(JSON.stringify(specifier)) as string,
cause: expect.objectContaining({
message: expect.stringContaining('failed to read installed DSH plugin package metadata') as string,
}) as Error,
})
await ctx.fiber.dispose()
})
})
describe('repository plugin invariant companion', () => {
it('registers its explained empty invariant', async () => {
const ctx = new Context()
await ctx.plugin(InvariantService, { enabled: true })
await expect(ctx.plugin(RepositoryPluginInvariant).await()).resolves.toBeDefined()
await ctx.fiber.dispose()
})
})

View File

@@ -1,33 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/loader"
},
{
"path": "../../skill/skill-local"
},
{
"path": "../../mcp/mcp-client"
},
{
"path": "../../util/paths"
},
{
"path": "../../support/invariants"
}
]
}

View File

@@ -1,17 +0,0 @@
import { defineConfig } from 'tsdown'
/** Build the runtime, invariant, and prepare executable as self-contained entries. */
export default defineConfig([
{
entry: ['lib/types/index.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024',
fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false,
},
{
entry: ['lib/types/invariant.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024',
fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false,
},
{
entry: ['lib/types/bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024',
fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false,
},
])

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/self-modification/tool-cordis/README.md
README.md: f2a65043a1d2f74553e98caf59ed3d38b5a70b7c
README.zh.md: 66742094992d219ccfbd60b935dcd10e48cb12b8
README.md: 4f856523cca4800cdbb98951183fbea1e3c96c87
README.zh.md: 7bb21396452ddbe49cf3008cd89cd6044b3c4a51

View File

@@ -14,7 +14,7 @@ Exact model-facing schemas: [the generated tool catalog](../../../docs/tool-cata
Canonical successes are the inspection string, mount `{ id, pluginName, state, provides, waitingFor }`, and unmount `{ id, pluginName }`. Native rendering says whether the temporary Plugin is running or pending and that it remains available until unmounted or DSH restarts; unmount confirms that it was removed.
Temporary Plugins live only in the shared DSH process memory. They remain active across later turns and may affect other sessions in that process, but disappear after `cordis_unmount`, toolset unload, or DSH restart. They create no Plugin file, install no package, change no `cordis.yml` or personal/project configuration, do not survive restart, and cannot be promoted automatically. To keep an experiment, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow.
Temporary Plugins live only in the shared DSH process memory. They remain active across later turns and may affect other sessions in that process, but disappear after `cordis_unmount`, toolset unload, or DSH restart. They create no Plugin file, install no package, change no `cordis.yml` or personal/project configuration, do not survive restart, and cannot be promoted automatically. To keep an experiment, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow.
## Trust stance

Some files were not shown because too many files have changed in this diff Show More