Merge remote-tracking branch 'origin/master' into worktree/custom-provider-edit-parity-f8c160

This commit is contained in:
Yichen Jiang
2026-08-11 15:16:23 +08:00
2325 changed files with 38307 additions and 17353 deletions

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: eb7df95bde10dafd7afcb168d30c9dda90296687
README.zh.md: 03cd02510267d3abdd414bed6ec1f42773d0811a

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 |
@@ -52,7 +52,6 @@ Groups hold `packages/<group>/<pkg>/`; names stay `@deepseek-ai/dsh-<pkg>`. **Gr
| [`boot/`](boot/README.md) | Shared app-bin boot glue | Product — stable surface |
| [`host/`](host/README.md) | Web-GUI host half: API gateway + HTTP route server | Product — stable surface |
| [`client/`](client/README.md) | Web-GUI browser half: shell, wire, object services, slots, `ui-*` plugins | Product — stable surface |
| [`experimental/`](experimental/README.md) | Prototypes and internal plugins | Unreleased |
| [`examples/`](examples/README.md) | Demo bundles (agent-spine + CLI/ACP/JSON-RPC bins) leaves load | Support — example infra |
| [`support/`](support/README.md) | Support infrastructure (testkits, invariants, replay, Loader smokes) | Support — lower compatibility expectations |
| [`util/`](util/README.md) | Low-level zero-dependency utilities shared across groups (`Branded<B>`, Harness home/path helpers, timeout, retention) | Support — small, stable, harness-dep-free |

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 全文搜索 | 产品:稳定接口 |
@@ -52,7 +52,6 @@ npm scope 为 `@deepseek-ai/dsh-*`Cordis `Service` 子类和函数插件通
| [`boot/`](boot/README.md) | 共享的 app bin 启动粘合层 | 产品:稳定接口 |
| [`host/`](host/README.md) | web GUI 宿主半侧API 网关 + HTTP 路由服务器 | 产品:稳定接口 |
| [`client/`](client/README.md) | web GUI 浏览器半侧shell、协议层、对象服务、slot、`ui-*` 插件 | 产品:稳定接口 |
| [`experimental/`](experimental/README.md) | 原型和内部插件 | 未发布 |
| [`examples/`](examples/README.md) | 演示组合包agent-spine + CLI/ACP/JSON-RPC bin由叶节点加载 | 支持:示例基础设施 |
| [`support/`](support/README.md) | 支持基础设施testkit、不变式、回放、Loader 冒烟测试) | 支持:兼容性预期较低 |
| [`util/`](util/README.md) | 组间共享的低层零依赖工具(`Branded<B>`、Harness home路径辅助函数、超时、保留策略 | 支持:小型、稳定、无 harness 依赖 |

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-acp",
"description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/acp/acp"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -26,14 +33,14 @@
"license": "BSD-3-Clause",
"dependencies": {
"@agentclientprotocol/sdk": "0.25.1",
"schemastery": "^3.17.0"
"@deepseek-ai/schemastery": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-user-approval": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -44,6 +51,6 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -9,11 +9,11 @@
* @module @deepseek-ai/dsh-acp
*/
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { randomUUID } from 'node:crypto'
import { isAbsolute } from 'node:path'
import { Readable, Writable } from 'node:stream'
import Schema from 'schemastery'
import Schema from '@deepseek-ai/schemastery'
import { createUserMessage, errorChain } from '@deepseek-ai/dsh-llm'
import {
AgentSideConnection,
@@ -252,6 +252,10 @@ export function apply(ctx: Context, config: AcpConfig): void {
assertOpen()
validateSessionParams(params)
const sessionId = SessionId(randomUUID())
// No preset composition: the ACP bundle keeps the model-facing rows in
// the host plane, so this agent reads them from the global layer. A
// deployment that configures a roster has to join one here first
// (@deepseek-ai/dsh-agent-presets README, "Composing a child agent").
const handle = await agents.create({
sessionId,
meta: { cwd: params.cwd },

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-acp'

View File

@@ -1,6 +1,6 @@
/** In-memory ACP transport fixture over the real agent factory and loop. */
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import {
ClientSideConnection,
ndJsonStream,

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-api-gateway",
"description": "TypeRT Remote Host dispatcher and Client API endpoint",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/api/gateway"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -52,17 +59,17 @@
"@deepseek-ai/dsh-type-meta": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/dsh-client-connection": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-typert-registry": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "workspace:^",
"zod": "^4.4.3"
}
}

View File

@@ -4,8 +4,8 @@
* participates in method lookup, invocation, or type exposure.
*/
import { Service } from 'cordis'
import type { Context } from 'cordis'
import { Service } from '@deepseek-ai/cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { ConnectionHandle, RpcError } from '@deepseek-ai/dsh-client-connection/client'
import type {
InvocationDescriptor,
@@ -53,7 +53,7 @@ interface RemoteNamespaceHandle {
/** Typed Remote service augmented by generated direct namespaces. */
export type ClientRemote = TypeRTClientRemote
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
/** Generated Remote namespaces selected by the Client assembly. */
remote: ClientRemote

View File

@@ -4,7 +4,7 @@
* @module @deepseek-ai/dsh-api-gateway
*/
import { Context, Service, symbols } from 'cordis'
import { Context, Service, symbols } from '@deepseek-ai/cordis'
import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection'
import {
remoteMethods,

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-api-gateway'

View File

@@ -46,7 +46,7 @@ export interface TypertGateway {
invoke(request: InvokeRemoteRequest): Promise<unknown>
}
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
/** Host dispatcher for TypeRT Remote calls. */
typertGateway: TypertGateway

View File

@@ -1,4 +1,4 @@
import { Context, Service } from 'cordis'
import { Context, Service } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import { z } from 'zod'
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'

View File

@@ -1,7 +1,7 @@
import { createServer } from 'node:http'
import type { AddressInfo } from 'node:net'
import { describe, expect, it } from 'vitest'
import { Context, Service, symbols } from 'cordis'
import { Context, Service, symbols } from '@deepseek-ai/cordis'
import { z } from 'zod'
import { apply as applyConnection, inject as connectionInject } from '@deepseek-ai/dsh-client-connection'
import type { HttpServerService, WebRoute } from '@deepseek-ai/dsh-host-webserver'

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-api-remotes",
"description": "Remote BFF assembly and Host Agent/Session lookup policy",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/api/remotes"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -46,13 +53,13 @@
"@deepseek-ai/dsh-type-meta": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-goal": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-session-persistence": "^0.0.1",
"@deepseek-ai/dsh-typert-registry": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -61,6 +68,6 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -1,6 +1,6 @@
/** Host BFF policy for resolving Remote Agent and Session identities. */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { Agent, AgentOptions, AgentSetup } from '@deepseek-ai/dsh-agent'
import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-session-persistence'

View File

@@ -1,13 +1,13 @@
/** Platform-neutral assembly of generated Host Remote contributions. */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import goalsRemote from '@deepseek-ai/dsh-goal/remote'
import type { TypeRTClientRemote } from '@deepseek-ai/dsh-type-meta'
export type { TypeRTClientRemote as ClientRemote } from '@deepseek-ai/dsh-type-meta'
export type {} from '@deepseek-ai/dsh-goal/remote'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
/** Generated Remote namespaces selected by this Client assembly. */
remote: TypeRTClientRemote

View File

@@ -1,7 +1,7 @@
/** Package-owned invariant companion for `@deepseek-ai/dsh-api-remotes`. */
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-api-remotes'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'

View File

@@ -45,7 +45,7 @@ describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
}).map(([key, path]) => [key, artifactUrl(path)]))
const script = `
import { createServer } from 'node:http'
import * as cordis from 'cordis'
import * as cordis from '@deepseek-ai/cordis'
const urls = ${JSON.stringify(urls)}
const { Context } = cordis
@@ -123,7 +123,7 @@ describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
const handoff = handoffs.get(id)
if (handoff === undefined) throw new Error('missing Client bundle handoff ' + id)
return handoff.factory(specifier => {
if (specifier === 'cordis') return cordis
if (specifier === '@deepseek-ai/cordis') return cordis
throw new Error('unexpected Client external ' + specifier)
})
}

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-attachment-local",
"description": "Private content-addressed DSH_HOME attachment storage",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/attachment/attachment-local"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -15,19 +22,19 @@
"files": ["lib/index.js", "lib/invariant.js", "lib/types/**/*.d.ts"],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-attachment": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-attachment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"schemastery": "^3.18.0",
"@deepseek-ai/schemastery": "workspace:^",
"sharp": "^0.35.3"
},
"devDependencies": {
"@deepseek-ai/dsh-attachment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -1,8 +1,8 @@
/** Local durable attachment backend rooted below `DSH_HOME`. @module @deepseek-ai/dsh-attachment-local */
import { join, resolve } from 'node:path'
import { Context } from 'cordis'
import z from 'schemastery'
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { AttachmentStore } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@deepseek-ai/dsh-attachment'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'

View File

@@ -1,7 +1,7 @@
/** Package-owned invariant companion for `@deepseek-ai/dsh-attachment-local`. @module @deepseek-ai/dsh-attachment-local/invariant */
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-attachment-local'

View File

@@ -1,4 +1,4 @@
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { existsSync } from 'node:fs'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-attachment",
"description": "Durable immutable attachment storage seam for the DeepSeek Harness",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/attachment/attachment"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -15,13 +22,13 @@
"files": ["lib/index.js", "lib/invariant.js", "lib/types/**/*.d.ts"],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-brand": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -1,6 +1,6 @@
/** Durable attachment storage seam (`ctx.attachments`). @module @deepseek-ai/dsh-attachment */
import { Context, Service } from 'cordis'
import { Context, Service } from '@deepseek-ai/cordis'
import type {
ImageAttachmentLimits,
ImageAttachmentRef,
@@ -19,7 +19,7 @@ export type {
StoredImageAttachment,
} from './types.ts'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
attachments: AttachmentStore
}

View File

@@ -1,7 +1,7 @@
/** Package-owned invariant companion for `@deepseek-ai/dsh-attachment`. @module @deepseek-ai/dsh-attachment/invariant */
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-attachment'

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/bash/bash-env/README.md
README.md: 7b939326d4effd14fc83ef0ad4e133f019f1011f
README.zh.md: 4d80d9d34f2be18e07d57d2427eb841f61f1ccfc
README.md: 54758a91773aad723c6fbbebe2ffe25aedb38599
README.zh.md: e0663b003e94942c76cde4fb7867ed0ce74c2aa2

View File

@@ -22,7 +22,7 @@ Every foreground and background model shell call receives a newly collected trus
`ctx.bashEnv` owns collection. Other plugins can register an effect-scoped contributor with a stable name, declared keys/descriptions, and `resolve(execution: ToolExecution)`; duplicate ownership and undeclared runtime keys fail loudly, while `list()` enumerates declarations without executing providers. Harness built-ins reserve `DSH_HOME`, `DSH_SHELL`, and `DSH_SESSION_ID`; this plugin's persistence translator owns `DSH_SESSION_JSONL` by reading the backend-neutral `sessionPersistence.locate()` seam.
```ts
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-bash-env'
export const inject = ['bashEnv']

View File

@@ -22,7 +22,7 @@
`ctx.bashEnv` 负责收集。其他插件可以注册一个受 effect 作用域约束的 contributor带有稳定名称、已声明的键/描述以及 `resolve(execution: ToolExecution)`;重复所有权与未声明的运行时键会响亮失败,而 `list()` 只枚举声明、不执行 provider。Harness 内置键保留 `DSH_HOME``DSH_SHELL``DSH_SESSION_ID`;本插件的持久化翻译器通过读取与后端无关的 `sessionPersistence.locate()` seam 拥有 `DSH_SESSION_JSONL`
```ts
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-bash-env'
export const inject = ['bashEnv']

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-bash-env",
"description": "Tool-independent managed DSH_* shell environment registry",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/bash/bash-env"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,15 +32,15 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-bash": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"@deepseek-ai/dsh-session-persistence": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -43,6 +50,6 @@
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -8,15 +8,15 @@
* @module @deepseek-ai/dsh-bash-env
*/
import { Service, type Context } from 'cordis'
import z from 'schemastery'
import { Service, type Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { DSH_ENV_PREFIX } from '@deepseek-ai/dsh-bash'
import type { DshEnvironment, DshEnvironmentKey } from '@deepseek-ai/dsh-bash'
import { DSH_HOME_ENV, resolveDshHome } from '@deepseek-ai/dsh-paths'
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-session-persistence'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
bashEnv: BashEnvRegistry
}

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-bash-env'

View File

@@ -7,7 +7,7 @@
import { homedir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { ToolExecution } from '@deepseek-ai/dsh-tools'

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-bash-local",
"description": "Local-subprocess implementation of the DeepSeek Harness bash executor seam",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/bash/bash-local"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,14 +32,14 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-bash": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-subprocess": "^0.0.1",
"@deepseek-ai/dsh-timeout": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-bash": "workspace:^",
@@ -40,6 +47,6 @@
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -9,8 +9,8 @@
* @module @deepseek-ai/dsh-bash-local
*/
import { Context } from 'cordis'
import z from 'schemastery'
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { BashExecutor } from '@deepseek-ai/dsh-bash'
import type { BashExecRequest, BashExecSpec, BashProcess, BashProcessRead, BashRunResult, CollectedOutput } from '@deepseek-ai/dsh-bash'
import type { SubprocessCollect, SubprocessHandle, SubprocessOutputReader, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-bash-local'

View File

@@ -2,7 +2,7 @@ import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-bash-sandbox",
"description": "Sandbox-consuming implementation of the DeepSeek Harness bash executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/bash/bash-sandbox"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,12 +32,12 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-bash": "^0.0.1",
"@deepseek-ai/dsh-bash-local": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-sandbox": "^0.0.1",
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-bash-local": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-bash": "workspace:^",
@@ -40,7 +47,7 @@
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/node-addon-landlock-run": "workspace:*"
}
}

View File

@@ -8,7 +8,7 @@
* @module @deepseek-ai/dsh-bash-sandbox
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash'
import { SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type {

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-bash-sandbox'

View File

@@ -4,7 +4,7 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
import { bwrapProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts'

View File

@@ -4,7 +4,7 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { homedir, tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { launcherPath } from '@deepseek-ai/node-addon-landlock-run'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'

View File

@@ -8,7 +8,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { LAUNCHER_FAILURE_EXIT } from '@deepseek-ai/node-addon-landlock-run'
import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'

View File

@@ -9,7 +9,7 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import type { BashRunResult, CollectedOutput } from '@deepseek-ai/dsh-bash'
import { SANDBOX_UNAVAILABLE, SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type { ConfinedArgv, SandboxExecutionPolicy, SandboxMode, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'

View File

@@ -4,7 +4,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
import { seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts'

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-bash",
"description": "Abstract bash executor seam (ctx.bash) for the DeepSeek Harness",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/bash/bash"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,15 +32,15 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-subprocess": "^0.0.1",
"@deepseek-ai/dsh-sandbox": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -5,7 +5,7 @@
* @module @deepseek-ai/dsh-bash
*/
import { Context, Service } from 'cordis'
import { Context, Service } from '@deepseek-ai/cordis'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from './types.ts'
@@ -25,7 +25,7 @@ export type {
export { parseExitStatus } from './render.ts'
export type { ParsedExitStatus } from './render.ts'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
bash: BashExecutor
}

View File

@@ -1,6 +1,6 @@
/** Package-owned invariant companion for the bash seam. @module @deepseek-ai/dsh-bash/invariant */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-bash'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { BashExecutor } from '@deepseek-ai/dsh-bash'
import type { BashExecRequest, BashExecSpec, BashProcess, BashProcessRead, BashRunResult } from '@deepseek-ai/dsh-bash'

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-pwsh-local",
"description": "Local PowerShell implementation of the DeepSeek Harness bash executor seam",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/bash/pwsh-local"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,14 +32,14 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-bash": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-subprocess": "^0.0.1",
"@deepseek-ai/dsh-timeout": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-bash": "workspace:^",
@@ -40,6 +47,6 @@
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -13,8 +13,8 @@
* @module @deepseek-ai/dsh-pwsh-local
*/
import { Context } from 'cordis'
import z from 'schemastery'
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { BashExecutor } from '@deepseek-ai/dsh-bash'
import type { BashExecRequest, BashExecSpec, BashProcess, BashProcessRead, BashRunResult, CollectedOutput } from '@deepseek-ai/dsh-bash'
import type { SubprocessCollect, SubprocessHandle, SubprocessOutputReader, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-pwsh-local'

View File

@@ -14,7 +14,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { spawnSync } from 'node:child_process'
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { PwshLocalExecutor, ENCODING_PREAMBLE, candidatePwshPaths, resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import SubprocessService from '@deepseek-ai/dsh-subprocess'

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/bash/pwsh-sandbox/README.md
README.md: bd506d011fa6167ddf7d6fe0565e475979ad0ec2
README.zh.md: e9aa380302037be3c9dd07331035544299bf3bec
README.md: c47409c122ffce264f53fc41c786da015fdaab6b
README.zh.md: 5b14185a71943d0a0a50f0bae353a3e22d0f1fb2

View File

@@ -30,5 +30,5 @@ None directly; the denial surface belongs to the tool layer.
## Known Limitations and Deferred Work
- **Reads are unrestricted** on Windows (the ACL runner restricts writes only); the read boundary is documented in `@deepseek-ai/dsh-sandbox-windows-acl`.
- **The Windows workspace-write temp area is the real temp directory** (`GetTempPathW`). This is a deliberate backend-defined choice, the same decision Landlock makes (`readWrite: ['/tmp', ...]`): the seam's "backend-defined temp area" permits it, and the escape probe in `tests/acl.e2e.ts` lives outside the temp tree for exactly that reason. A per-run private temp (bwrap's `--tmpfs /tmp` semantics) would additionally need an environment-block rewrite in the runner; it is an optional future hardening, not a correctness gap.
- **Windows read-only is strict zero-grant** — not even the NUL device is writable; `> $null` redirection still works (documented in the backend package).
- **Windows workspace-write temp authority is private** per live session/workspace pair; agentless calls receive a fresh private directory per invocation. The ambient temp root is never granted, and the runner rewrites TMP/TEMP to the private directory before spawning.
- **Windows read-only grants no explicit writable root but remains partial** because the restricted token must retain Everyone. Objects whose DACL grants Everyone write access — including compatible opens of the NUL device — remain ambient authority; PowerShell's `> $null` redirection still works without opening NUL.

View File

@@ -30,5 +30,5 @@
## 已知限制与后续工作
- **Windows 上读不受限**ACL runner 只限写);读边界文档在 `@deepseek-ai/dsh-sandbox-windows-acl`
- **Windows workspace-write 的临时区域是真实临时目录**`GetTempPathW`)。这是有意为之的后端自定义选择,与 Landlock 的决策(`readWrite: ['/tmp', ...]`同类seam 的 "backend-defined temp area" 词汇表允许它,`tests/acl.e2e.ts` 的逃逸探针也正是因此位于 temp 树之外。按运行创建私有临时目录bwrap `--tmpfs /tmp` 的语义)还需 runner 改写环境块——这是可选的进一步加固,而非正确性缺口
- **Windows read-only 是严格零授权**——连 NUL 设备都不可写;`> $null` 重定向不受影响(后端包有文档)
- **Windows workspace-write 的临时权限按每个活跃的会话/工作区对私有**;无 agent智能体的调用每次都获得一个新的私有目录。环境临时目录绝不会被授权runner 会在 spawn 前将 TMP/TEMP 重写为该私有目录
- **Windows read-only 不授予任何显式可写根目录,但仍为部分强制执行**,因为受限令牌必须保留 Everyone。DACL 向 Everyone 授予写访问的对象——包括以兼容方式打开的 NUL 设备——仍构成环境权限来源PowerShell 的 `> $null` 重定向仍可工作,且不会打开 NUL

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-pwsh-sandbox",
"description": "Sandbox-consuming implementation of the DeepSeek Harness PowerShell executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/bash/pwsh-sandbox"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,12 +32,12 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-bash": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-pwsh-local": "^0.0.1",
"@deepseek-ai/dsh-sandbox": "^0.0.1",
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-bash": "workspace:^",
@@ -40,6 +47,6 @@
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -12,7 +12,7 @@
* @module @deepseek-ai/dsh-pwsh-sandbox
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash'
import { SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type {

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-pwsh-sandbox'

View File

@@ -2,9 +2,9 @@
* Real-backend end-to-end: LocalSandboxProvider (win32 chain → the
* windows-acl runner), SandboxPolicyService, and SandboxPwshExecutor with
* REAL pwsh spawns confined through the runner — the debug-instance
* verification of both modes: read-only denies every write (not even NUL),
* workspace-write allows the workspace and temp while denying escape writes,
* and denial/classification facts ride the settled result.
* verification of both modes on ordinary user-owned paths: read-only denies
* writes, workspace-write allows its promised roots while denying escape
* writes, and the partial-enforcement/denial facts ride the settled result.
*/
import { spawnSync } from 'node:child_process'
@@ -12,7 +12,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:
import { homedir, tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'
import { resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
@@ -29,21 +29,19 @@ function pwshAvailable(): boolean {
describe.skipIf(!isWin32 || !pwshAvailable())('pwsh-sandbox real ACL confinement', () => {
let scratchRoot!: string
let writableDir!: string
let isolatedTemp!: string
let outsideTempDir!: string
let secretFile!: string
let escapeFile!: string
let executor!: SandboxPwshExecutor
beforeAll(async () => {
// The escape probe must live OUTSIDE every legitimately granted tree: the
// provider's workspace-write grants the workspace plus the REAL temp dir
// (the 'backend-defined temp area', same as Landlock granting /tmp), so a
// scratch dir under temp would inherit the grant and the probe would be a
// false pass. A mkdtemp under the profile is removed by afterAll.
// The workspace escape sits under the profile. A separate directory under
// the ambient temp root proves that the root itself is not granted: the
// runner creates its own private child and rewrites TMP/TEMP to it.
scratchRoot = mkdtempSync(join(homedir(), 'dsh-pwsh-sandbox-e2e-'))
writableDir = join(scratchRoot, 'writable')
mkdirSync(writableDir)
isolatedTemp = mkdtempSync(join(tmpdir(), 'dsh-pwsh-sandbox-e2e-temp-'))
outsideTempDir = mkdtempSync(join(tmpdir(), 'dsh-pwsh-sandbox-e2e-outside-temp-'))
secretFile = join(scratchRoot, 'secret.txt')
writeFileSync(secretFile, 'top secret - must stay readable to prove the read boundary')
escapeFile = join(scratchRoot, 'escaped.txt')
@@ -58,15 +56,15 @@ describe.skipIf(!isWin32 || !pwshAvailable())('pwsh-sandbox real ACL confinement
afterAll(() => {
rmSync(scratchRoot, { recursive: true, force: true })
rmSync(isolatedTemp, { recursive: true, force: true })
rmSync(outsideTempDir, { recursive: true, force: true })
})
it('read-only: every write denied (workspace, temp, NUL), reads fine, denial facts ride the result', async () => {
it('read-only: ordinary path writes denied, reads fine, partial and denial facts ride the result', async () => {
const policy: SandboxExecutionPolicy = { mode: 'read-only', workspaceRoot: writableDir }
const probe = [
"$ErrorActionPreference='SilentlyContinue';",
`try{Set-Content -Path '${writableDir}\\ro-write.txt' -Value ok -ErrorAction Stop;'TARGET-WRITE: OK'}catch{'TARGET-WRITE: DENIED'};`,
`try{Set-Content -Path '${isolatedTemp}\\ro-write.txt' -Value ok -ErrorAction Stop;'TEMP-WRITE: OK'}catch{'TEMP-WRITE: DENIED'};`,
`try{Set-Content -Path '${outsideTempDir}\\ro-write.txt' -Value ok -ErrorAction Stop;'TEMP-WRITE: OK'}catch{'TEMP-WRITE: DENIED'};`,
`try{Set-Content -Path '${escapeFile}' -Value ok -ErrorAction Stop;'ESCAPE-WRITE: OK'}catch{'ESCAPE-WRITE: DENIED'};`,
`try{Get-Content '${secretFile}' -ErrorAction Stop | Out-Null;'SECRET-READ: OK'}catch{'SECRET-READ: DENIED'}`,
].join('')
@@ -78,7 +76,7 @@ describe.skipIf(!isWin32 || !pwshAvailable())('pwsh-sandbox real ACL confinement
expect(result.stdout.text).toContain('SECRET-READ: OK')
expect(existsSync(join(writableDir, 'ro-write.txt'))).toBe(false)
// A self-caught denial keeps the command exit 0: no denial fact.
expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'full' })
expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'partial' })
// A raw failing write must classify as a denial of the ACL dialect.
const denied = await executor.run(executor.resolve({
@@ -86,26 +84,34 @@ describe.skipIf(!isWin32 || !pwshAvailable())('pwsh-sandbox real ACL confinement
sandboxPolicy: policy,
}))
expect(denied.exitCode).not.toBe(0)
expect(denied.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
expect(denied.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'partial' })
}, 60_000)
it('workspace-write: workspace and temp writable, escape denied, reads fine', async () => {
it('workspace-write: workspace and private temp writable, ambient temp and escape denied', async () => {
const policy: SandboxExecutionPolicy = { mode: 'workspace-write', workspaceRoot: writableDir }
const probe = [
"$ErrorActionPreference='SilentlyContinue';",
`try{Set-Content -Path '${writableDir}\\ww-write.txt' -Value ok -ErrorAction Stop;'TARGET-WRITE: OK'}catch{'TARGET-WRITE: DENIED'};`,
`try{Set-Content -Path '${isolatedTemp}\\ww-write.txt' -Value ok -ErrorAction Stop;'TEMP-WRITE: OK'}catch{'TEMP-WRITE: DENIED'};`,
"try{Set-Content -Path (Join-Path $env:TEMP 'ww-write.txt') -Value ok -ErrorAction Stop;'TEMP-WRITE: OK'}catch{'TEMP-WRITE: DENIED'};",
`try{Set-Content -Path '${outsideTempDir}\\ww-write.txt' -Value ok -ErrorAction Stop;'AMBIENT-TEMP-WRITE: OK'}catch{'AMBIENT-TEMP-WRITE: DENIED'};`,
`try{Set-Content -Path '${escapeFile}' -Value ok -ErrorAction Stop;'ESCAPE-WRITE: OK'}catch{'ESCAPE-WRITE: DENIED'};`,
`try{Get-Content '${secretFile}' -ErrorAction Stop | Out-Null;'SECRET-READ: OK'}catch{'SECRET-READ: DENIED'}`,
`try{Get-Content '${secretFile}' -ErrorAction Stop | Out-Null;'SECRET-READ: OK'}catch{'SECRET-READ: DENIED'};`,
"'TEMP-PATH: ' + $env:TEMP",
].join('')
const result = await executor.run(executor.resolve({ command: probe, sandboxPolicy: policy }))
expect(result.exitCode, `stderr: ${result.stderr.text}`).toBe(0)
expect(result.stdout.text).toContain('TARGET-WRITE: OK')
expect(result.stdout.text).toContain('TEMP-WRITE: OK')
expect(result.stdout.text).toContain('AMBIENT-TEMP-WRITE: DENIED')
expect(result.stdout.text).toContain('ESCAPE-WRITE: DENIED')
expect(result.stdout.text).toContain('SECRET-READ: OK')
expect(existsSync(join(writableDir, 'ww-write.txt'))).toBe(true)
expect(existsSync(join(outsideTempDir, 'ww-write.txt'))).toBe(false)
expect(existsSync(escapeFile)).toBe(false)
expect(result.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' })
const privateTemp = result.stdout.text.match(/^TEMP-PATH: (.+)$/mu)?.[1]?.trim()
expect(privateTemp).toBeDefined()
expect(privateTemp?.startsWith(tmpdir())).toBe(true)
expect(existsSync(privateTemp ?? '')).toBe(false)
expect(result.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'partial' })
}, 60_000)
})

View File

@@ -11,7 +11,7 @@ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, describe, expect, it } from 'vitest'
import { Context, Service } from 'cordis'
import { Context, Service } from '@deepseek-ai/cordis'
import { SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type { ConfinedArgv, RunnerFailureRule, SandboxExecutionPolicy, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
import { resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-tool-bash",
"description": "Model-facing bash tool with optional generic background-task and sandbox-escalation support",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/bash/tool-bash"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,21 +32,21 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-bash": "^0.0.1",
"@deepseek-ai/dsh-bash-env": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-sandbox": "^0.0.1",
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-tasks": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"@deepseek-ai/dsh-user-approval": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-bash-env": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -61,6 +68,6 @@
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -8,8 +8,8 @@
* @module @deepseek-ai/dsh-tool-bash
*/
import type { Context } from 'cordis'
import z from 'schemastery'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { isAbsolute, resolve as resolvePath } from 'node:path'
import { defineTool, TOOL_ABORTED } from '@deepseek-ai/dsh-tools'
import type { GenericCallView, TerminalCallView, ToolExecution, ToolResult, ToolResultView } from '@deepseek-ai/dsh-tools'

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-tool-bash'

View File

@@ -1,6 +1,6 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

View File

@@ -2,7 +2,7 @@ import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import { BashExecutor } from '@deepseek-ai/dsh-bash'
import type { BashExecRequest, BashExecSpec, BashProcess, BashProcessRead, BashRunResult } from '@deepseek-ai/dsh-bash'

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/bash/tool-pwsh/README.md
README.md: 3fd5a53946e2b101d6ef4457e312e52f4db5f3a8
README.zh.md: c06b4354b6973a6ff196cda7c49966c7c40e0a90
README.md: 4126e718c569f17fb8be465351b2576970e93c63
README.zh.md: aba669733a7ecb9924287abb298bb9a154b5afa7

View File

@@ -120,7 +120,7 @@ Append-only; newly visible content follows the reusable request prefix and does
## Known Limitations and Deferred Work
- **ConstrainedLanguage and named-pipe capture under the Windows sandbox** — when the [Windows ACL sandbox](../../sandbox/sandbox-windows-acl/README.md) confines a call (read-only or workspace-write), the restricted token puts pwsh into ConstrainedLanguage mode: `Add-Type`, non-core .NET statics (`[System.IO.*]::`, `[math]::`), COM objects, and reflection fail with "only core types" errors, and the mode cannot be lifted from inside. The same modes deny named-pipe opens, so a piped-stdio spawn inside a confined command fails with EPERM. The tool description teaches both contracts to the model; the backend README owns the full limitations.
- **Language mode and named-pipe capture under the Windows sandbox** — under the [Windows ACL sandbox](../../sandbox/sandbox-windows-acl/README.md), read-only pwsh starts in ConstrainedLanguage because its temp write denial makes PowerShell's AppLocker probe fail closed: `Add-Type`, non-core .NET statics (`[System.IO.*]::`, `[math]::`), COM objects, and reflection fail with "only core types" errors, and the mode cannot be lifted from inside. Workspace-write's private temp lets the probe complete, so it stays in FullLanguage unless host policy says otherwise. Both confined modes deny named-pipe opens, so a piped-stdio spawn inside a confined command fails with EPERM. The tool description teaches both contracts to the model; the backend README owns the full limitations.
- **No persistent shell or PTY** — every call starts a fresh `pwsh -Command`; the PTY backends are Linux/macOS-only today, and a Windows ConPTY persistent shell is roadmap work.
- **PowerShell-dialect contract** — the model must write PowerShell (native paths, `$env:` variables), not bash; there is no dialect translation.
- **Session-cwd identity is not canonicalized** — the workdir base is the session header cwd as-is, unlike the bash tool's sandbox-root-canonicalized identity. Under a confining executor the policy's workspace root IS canonicalized (by the shared policy service), so the workdir and the confinement root can diverge when the raw session cwd differs from its canonical form — a parity gap deferred to the shared shell-tool base extraction.

View File

@@ -120,7 +120,7 @@ ack 是固定短行;任务输出按读取有界。
## Known Limitations and Deferred Work
- **Windows sandbox 下的 ConstrainedLanguage 与 named-pipe 捕获** — [Windows ACL sandbox](../../sandbox/sandbox-windows-acl/README.md) 隔离某次调用(read-only 或 workspace-write受限令牌使 pwsh 进入 ConstrainedLanguage 模式`Add-Type`、非核心 .NET 静态调用(`[System.IO.*]::``[math]::`、COM 对象与反射都会以“only core types”错误失败且该模式无法从内部解除。这两种模式同样会拒绝 named-pipe 打开,因此受限命令内的管道 stdio spawn 以 EPERM 失败。工具描述把这两个约定教给模型;后端 README 负责完整的限制说明。
- **Windows 沙箱下的语言模式与 named-pipe 捕获** — [Windows ACL 沙箱](../../sandbox/sandbox-windows-acl/README.md) 下,read-only pwsh 会以 ConstrainedLanguage 启动,因为临时目录写入被拒绝,导致 PowerShell 的 AppLocker 探针失败并按 fail-closed 处理`Add-Type`、非核心 .NET 静态调用(`[System.IO.*]::``[math]::`、COM 对象与反射都会以“only core types”错误失败且该模式无法从内部解除。workspace-write 的私有临时目录使探针得以完成,因此除非主机策略另有规定,否则它保持 FullLanguage。两种受限模式都拒绝 named-pipe 打开,因此受限命令内的管道 stdio spawn 以 EPERM 失败。工具描述把这两个约定教给模型;后端 README 负责完整的限制说明。
- **无持久 shell 或 PTY** — 每次调用都启动全新的 `pwsh -Command`PTY 后端目前仅限 Linux/macOSWindows ConPTY 持久 shell 属于路线图工作。
- **PowerShell 方言约定** — 模型必须写 PowerShell原生路径、`$env:` 变量),而不是 bash没有方言翻译。
- **会话 cwd 身份不做规范化** — workdir 基座直接取会话头 cwd 原值,不同于 bash 工具经 sandbox-root 规范化的身份。在隔离执行器下,策略的工作区根**会**被规范化(由共享的策略服务完成),因此当原始会话 cwd 与其规范化形态不同时workdir 与隔离根可能不一致——这一 parity 差距留待共享 shell 工具基座提取时解决。

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-tool-pwsh",
"description": "Model-facing pwsh tool over the bash executor seam",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/bash/tool-pwsh"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,21 +32,21 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-bash": "^0.0.1",
"@deepseek-ai/dsh-bash-env": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-sandbox": "^0.0.1",
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-tasks": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"@deepseek-ai/dsh-user-approval": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-bash-env": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -58,6 +65,6 @@
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -20,8 +20,8 @@
*/
import { isAbsolute, resolve as resolvePath } from 'node:path'
import type { Context } from 'cordis'
import z from 'schemastery'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { defineTool, TOOL_ABORTED } from '@deepseek-ai/dsh-tools'
import type { GenericCallView, TerminalCallView, ToolExecution, ToolResult, ToolResultView } from '@deepseek-ai/dsh-tools'
import { HarnessError } from '@deepseek-ai/dsh-llm'
@@ -114,18 +114,18 @@ function pwshDescription(backgroundEnabled: boolean, escalationModes: readonly S
+ 'On Windows a force-killed command settles as `[exit code: 1]` without a signal marker — treat it as an interruption, not a command failure. '
+ background
if (escalationModes.length === 0) return base
// The CLM and named-pipe contracts below are Windows-restricted-token
// The language-mode and named-pipe contracts below are Windows-restricted-token
// behavior, but the gate is 'any confining executor is mounted'
// (escalationModes non-empty). The conflation is safe today because every
// shipped composition pairing tool-pwsh with a confining executor is
// win32-only; a future POSIX pwsh-sandbox composition must gate both
// sentences on the platform instead (tracked in the pwsh-tool-and-executor
// Agent Note).
return base + ' Under the Windows sandbox, pwsh runs in PowerShell ConstrainedLanguage mode (read-only and '
+ 'workspace-write): prefer cmdlets and core types (`[string]`, `[datetime]`, `[regex]`, `[guid]`); '
return base + ' Under the Windows sandbox, read-only pwsh runs in PowerShell ConstrainedLanguage mode, while '
+ 'workspace-write stays in FullLanguage unless host policy says otherwise. In read-only, prefer cmdlets and core types (`[string]`, `[datetime]`, `[regex]`, `[guid]`); '
+ '.NET static calls (`[System.IO.*]::`, `[math]::`), `Add-Type`, COM objects, and reflection fail '
+ 'with "only core types" errors. `-f` formatting, property access, and core cmdlets work. '
+ 'In the same modes, programs cannot open named pipes, so a command that captures another '
+ 'In both confined modes, programs cannot open named pipes, so a command that captures another '
+ 'program\'s output through piped stdio (Node.js `child_process.spawn`/`exec` with the default '
+ '`stdio: \'pipe\'`) fails with EPERM, while `stdio: \'inherit\'` and `stdio: \'ignore\'` spawns '
+ 'work and PowerShell\'s own pipelines are unaffected. That EPERM is the documented boundary: '

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-tool-pwsh'

View File

@@ -14,7 +14,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { spawnSync } from 'node:child_process'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { TOOL_ABORTED } from '@deepseek-ai/dsh-tools'

View File

@@ -11,7 +11,7 @@
*/
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { mkdtempSync, realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve as resolvePath } from 'node:path'
@@ -559,7 +559,8 @@ describe('sandbox escalation through ctx.approval', () => {
expect(properties['sandbox_permissions']?.enum).toEqual(['workspace-write', 'danger-full-access'])
expect(schema.description).toContain('approval prompt')
expect(schema.description).toContain('ConstrainedLanguage')
expect(schema.description).toContain('named pipes')
expect(schema.description).toContain('workspace-write stays in FullLanguage')
expect(schema.description).toContain('In both confined modes, programs cannot open named pipes')
expect(schema.description).toContain('fails with EPERM')
for (const args of [

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/boot/README.md
README.md: 5e4e483b60adab0b22ddb5279f4cd8fb699b9c35
README.zh.md: 95a3f98129a7d1fdfaffb3cac6fed77bab7cff56
README.md: 79d653260ea4a9d9a4c71a593b41a6a7e17efa14
README.zh.md: 839be164328ef168cd6ac18bf2f1dcb930dfce3e

View File

@@ -7,5 +7,6 @@ The channel-neutral boot library the app bins share: `apps/cli`, the [`scaffold/
| Package | Role | ctx key |
|---|---|---|
| `app-boot/` | Shared boot glue for the app bins: `.env` loading, fail-loud Loader guards, snapshot-aware config resolution, the settle-the-tree boot sequence | (library for the bins) |
| `cmdline/` | Launcher-to-app command-line handoff and app-owned startup parsing | `cmdlineArgs`, `appExit` |
The boot sequence and personal-config contract are documented in [`app-boot/README.md`](app-boot/README.md).
The boot sequence and personal-config contract are documented in [`app-boot/README.md`](app-boot/README.md); app-owned command lines are documented in [`cmdline/README.md`](cmdline/README.md).

View File

@@ -7,5 +7,6 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
| `app-boot/` | app bin 的共享启动粘合层:加载 `.env`、会明确报错的 Loader 保护机制、感知快照的配置解析,以及等待整棵树停稳的启动序列 | (供各 bin 使用的库) |
| `cmdline/` | 启动器到应用的命令行交接,以及由应用持有的启动解析 | `cmdlineArgs``appExit` |
启动序列与个人配置约定见 [`app-boot/README.md`](app-boot/README.md)。
启动序列与个人配置约定见 [`app-boot/README.md`](app-boot/README.md);由应用持有的命令行见 [`cmdline/README.md`](cmdline/README.md)

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/boot/app-boot/README.md
README.md: be03bceb39935fafb7acc7d3a99c1fe3af686f94
README.zh.md: 10165486712fc078cdf1f4147522397a15c88955
README.md: 9639f1c0a2ffe91fd509a2ffdf04be5f0895b700
README.zh.md: e8bf0374aad2be2311b6e72e91e41f02f403b48a

View File

@@ -15,10 +15,10 @@ Shared boot glue for the app bins ([`dsh`](../../../apps/cli/README.md) and [`ds
| `assertEntriesActivated(ctx, binName)` | Include the `assertEntriesLoaded` check, then await every enabled entry after the Loader settles; throw with each failed plugin's original stack or each pending plugin's unresolved services |
| `loadOptionalPatches(binName, file)` | Parse an optional patch-list file (a profile's `cordis.patch.yml`) — a top-level YAML array of include `PatchOptions` (id-targeted config overrides, `insert` lists, `!!js` allowed); absent file → `undefined`, an unreadable/unparsable/non-array file throws |
| `loadOverlayPatches(binName, file)` | Parse a required top-level YAML array containing the same include `PatchOptions` entries described above; a missing file also throws because the caller named it |
| `mountRootInclude(ctx, absoluteConfigPath, patches?)` | Register the statically imported `cordis:include` and `cordis:group` builtins, mount the include, and retain the exact root entry used by user patch-layer HMR |
| `mountRootInclude(ctx, absoluteConfigPath, patches?, bareModuleBaseUrl?)` | Register the statically imported `cordis:include` and `cordis:group` builtins, mount the include, and retain the exact root entry used by user patch-layer HMR; an optional module base anchors bare package names to the installed host while relative names stay config-relative |
| `watchUserPatches(ctx, options)` | Register the named patch file with the existing Cordis HMR service; each add/change/removal transactionally recomposes the full patch list through the caller's `compose` closure (app-owned layers around the current user layer) and returns an async disposer |
| `resolveProfileDir` / `initProfile` / `loadProfile` / `readProfileManifest` / `writeProfileManifest` / `resolveBundleDir` / `composeEntries` / `healProfilesModuleFallback` / `PROFILE_TEMPLATES` / `DEFAULT_PROFILE_BUNDLES` / `PROFILES_DIR` / `PROFILE_PATCH_FILENAME` | Profile machinery (see [Profiles](#profiles)) |
| `boot(binName, absoluteConfigPath, patches?, prepare?)` | Create the root context, expose `dshHomePath(...segments)` to Loader `!!js` config expressions, install Loader, run optional host preparation before config-tree entries mount (`prepare` may use Loader and provide launcher-owned context slots), then mount and await the include tree, assert entries loaded and activated, and return the root context — or dispose the partial context and reject a labelled error |
| `boot(binName, absoluteConfigPath, patches?, prepare?, bareModuleBaseUrl?)` | Create the root context, expose `dshHomePath(...segments)` to Loader `!!js` config expressions, install Loader, run optional host preparation before config-tree entries mount (`prepare` may use Loader and provide launcher-owned context slots), then mount and await the include tree, assert entries loaded and activated, and return the root context — or dispose the partial context and reject a labelled error; the optional module base has the same resolution semantics as `mountRootInclude` |
| `renderConfigDump(binName, absoluteConfigPath, layers, warn?)` | Compose the base config and labeled overlay layers offline with the include's own parser and patch algorithm (`entryListSchema`/`applyEntryPatches`), so the result equals what `boot()` mounts, and render YAML with `!!js` expressions verbatim; each run of rows that shares one source file and the same patch layers is preceded by a `# ==` comment naming that file and those layers, keeping the output one loadable document; a patch matching no row goes to `warn` with its layer label (default: one stderr line), and read, parse, or field validation failures throw |
| `addHarnessSourceSection(ctx, sourceRoot)` | Add a global `harness:source` prompt section (ordered just after the harness identity, before the persona) telling the agent the on-disk path to the DSH implementation checkout while warning it not to infer the current working directory from that path and to use `pwd` instead; a no-op returning `undefined` when the booted tree has no `systemPrompt` service. The section is registered against that service's fiber, so a dev HMR reload of the system prompt drops it until the next boot |
| `HARNESS_SOURCE_SECTION` | The `'harness:source'` section name `addHarnessSourceSection` registers under |
@@ -29,7 +29,7 @@ The Loader mounts entries concurrently, so a surface can already own the termina
`cordis:group` is registered beside `cordis:include` so a composition can give one `isolate` realm to a provider and its consumers together. Both load through the ambient module pipeline rather than the included tree's own specifier resolution, which is what lets a composition outside this workspace — an agent preset under the Harness home — use a group row at all.
Bare plugin specifiers in a config (`@deepseek-ai/dsh-*`, npm packages) resolve through the Cordis Loader's internal module loader. Repository bins install Loader's optional `node-addon-require-builtin` peer; external callers must supply it or install plugins where plain Node import resolution can find them. Relative specifiers resolve against the config directory without the native helper. The built `dsh-app-boot` artifact embeds the statically mounted Include implementation while leaving Loader external, so the include tree and host bind to one Loader peer. The `dsh` source launcher additionally maps manifest-declared workspace packages to their TypeScript source; its configuration gate requires every shipped raw/Web bare plugin to appear in the resolver manifest's `dependencies`.
Bare plugin specifiers in a config (`@deepseek-ai/dsh-*`, npm packages) resolve through the Cordis Loader's internal module loader. They resolve from the config directory by default; a closed runtime passes `bareModuleBaseUrl` to `boot` or `mountRootInclude` so its installed package tree remains authoritative even when the config lives inside another Node project. Relative specifiers always resolve against the config directory. Repository bins install Loader's optional `node-addon-require-builtin` peer; external callers must supply it or install plugins where plain Node import resolution can find them. The built `dsh-app-boot` artifact embeds the statically mounted Include implementation while leaving Loader external, so the include tree and host bind to one Loader peer. The `pnpm dsh` source path additionally maps manifest-declared workspace packages to their TypeScript source; its configuration gate requires every shipped raw/Web bare plugin to appear in the resolver manifest's `dependencies`.
This package carries no loader hooks and no dev-mode surface. The [`dsh` app](../../../apps/cli/README.md) owns its Node source-launch hook and consumes these helpers for the boot sequence; built consumers continue to use plain Node package resolution.
@@ -42,7 +42,7 @@ User-level machine-local preferences also live in the Harness home:
- **`.env`** — the product CLI's ordinary environment layers: the invoking directory's file outranks the Harness-home file, and both sit below the inherited environment. `loadLayeredEnv` snapshots each value's source, rejects [bootstrap-only file variables](../../../.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md#decision) case-insensitively, and materializes accepted values into `process.env` for Loader expressions and third-party libraries. Managed credentials live separately in [`.credentials.yaml`](../../credentials/credentials-local/README.md); a credential left in either `.env` remains a lower-priority fallback.
- **`cordis.patch.yml`** (home level) and **`profiles/<name>/cordis.patch.yml`** — the user patch layers, applied after every bundle layer (per-profile first, then the home-level file, which therefore outranks it): an id-targeted patch replaces the named entry's whole `config` (restate unchanged fields), `insert` adds entries, and `!!js` expressions interpolate at mount. A patch naming an entry id absent from the composed tree is a stderr warning. An empty or comments-only file throws (it parses to nothing, not to a list); disable the layer with `[]`.
Long-lived surfaces keep `cordis.patch.yml` live through `watchUserPatches`; one-shot runs read only the startup value. The watcher targets the exact path even when the file or immediate parent does not exist, serializes bursts, and recomposes the user patches inside the caller's layer order (bundle layers below, overlay/flag patches above). A rejected read, parse, or Loader candidate leaves the last good tree running and the HMR service broadcasts `hmr/config-update-failed(filename, Error)` after logging it; observer failures are contained. Disposing the context closes the watcher and drains an active refresh.
Long-lived surfaces keep `cordis.patch.yml` live through `watchUserPatches`; one-shot runs read only the startup value. The watcher targets the exact path even when the file or immediate parent does not exist, serializes bursts, and recomposes the user patches inside the caller's layer order (bundle layers below, overlays above). A rejected read, parse, or Loader candidate leaves the last good tree running and the HMR service broadcasts `hmr/config-update-failed(filename, Error)` after logging it; observer failures are contained. Disposing the context closes the watcher and drains an active refresh.
## Model Experience

View File

@@ -15,10 +15,10 @@
| `assertEntriesActivated(ctx, binName)` | 先执行 `assertEntriesLoaded` 检查,再在 Loader 结算后等待每个已启用配置项;抛出的错误包含每个失败插件的原始错误堆栈,或每个等待中插件尚未解析的服务 |
| `loadOptionalPatches(binName, file)` | 解析一份可选的 patch 列表文件(即 profile 的 `cordis.patch.yml`):其顶层是一个 YAML 数组,内容为 include 的 `PatchOptions`(按 id 定位的配置覆盖、`insert` 列表,允许 `!!js`);文件不存在时返回 `undefined`,文件不可读、不可解析或内容不是数组时抛出异常 |
| `loadOverlayPatches(binName, file)` | 解析必需的顶层 YAML 数组,其中包含与上文相同的 include `PatchOptions` 条目;文件缺失也会抛出异常,因为该文件是调用方指名的 |
| `mountRootInclude(ctx, absoluteConfigPath, patches?)` | 注册静态导入的 `cordis:include``cordis:group` builtin挂载 include并保留用户 patch 层 HMR热模块替换使用的确切根配置项 |
| `mountRootInclude(ctx, absoluteConfigPath, patches?, bareModuleBaseUrl?)` | 注册静态导入的 `cordis:include``cordis:group` builtin挂载 include并保留用户 patch 层 HMR热模块替换使用的确切根配置项;可选模块基准会把裸包名锚定到已安装宿主,而相对名称仍以配置目录为基准 |
| `watchUserPatches(ctx, options)` | 向现有 Cordis HMR 服务注册指名的 patch 文件;每次新增、变更或移除都会通过调用方的 `compose` 闭包(应用自有层围绕当前用户层)以事务方式重新组合完整 patch 列表,并返回异步清理函数 |
| `resolveProfileDir` / `initProfile` / `loadProfile` / `readProfileManifest` / `writeProfileManifest` / `resolveBundleDir` / `composeEntries` / `healProfilesModuleFallback` / `PROFILE_TEMPLATES` / `DEFAULT_PROFILE_BUNDLES` / `PROFILES_DIR` / `PROFILE_PATCH_FILENAME` | Profile 机制(见 [Profile](#profiles) |
| `boot(binName, absoluteConfigPath, patches?, prepare?)` | 创建根上下文,向 Loader `!!js` 配置表达式暴露 `dshHomePath(...segments)` 并安装 Loader在配置树条目挂载前执行可选的宿主准备操作`prepare` 可以使用 Loader也可以提供由启动器拥有的上下文插槽再挂载并等待 include 树结算,断言所有条目均已加载并激活,最后返回根上下文——失败时 dispose资源释放部分构造的上下文并以带标签的错误 reject |
| `boot(binName, absoluteConfigPath, patches?, prepare?, bareModuleBaseUrl?)` | 创建根上下文,向 Loader `!!js` 配置表达式暴露 `dshHomePath(...segments)` 并安装 Loader在配置树条目挂载前执行可选的宿主准备操作`prepare` 可以使用 Loader也可以提供由启动器拥有的上下文插槽再挂载并等待 include 树结算,断言所有条目均已加载并激活,最后返回根上下文——失败时 dispose资源释放部分构造的上下文并以带标签的错误 reject;可选模块基准与 `mountRootInclude` 的解析语义相同 |
| `renderConfigDump(binName, absoluteConfigPath, layers, warn?)` | 使用 include 自己的解析器和补丁算法(`entryListSchema`/`applyEntryPatches`)离线合成基础配置与带标签的覆盖层,使结果与 `boot()` 挂载的内容一致,再渲染为 YAML并原样保留 `!!js` 表达式;每段来源于同一文件且由相同补丁层修改的连续行之前都有一条 `# ==` 注释,标明该文件和这些补丁层,输出仍是一份可加载的文档;未匹配到行的补丁连同其层标签交给 `warn`(默认:一行 stderr读取、解析或字段验证失败则抛出 |
| `addHarnessSourceSection(ctx, sourceRoot)` | 添加全局 `harness:source` 提示词段落(顺序紧随 harness 身份、位于 persona 之前),告知 agent智能体DSH 实现代码 checkout 的磁盘路径,同时提醒它不得据此推断当前工作目录,而应使用 `pwd`;如果已启动树没有此项服务,则不执行操作并返回 `undefined`。这里的服务是 `systemPrompt`;该段落注册到它的 fiber因此开发环境 HMR热模块替换重新加载系统提示词后它会消失直至下次启动 |
| `HARNESS_SOURCE_SECTION` | `'harness:source'` 段落名称,供 `addHarnessSourceSection` 注册使用 |
@@ -29,7 +29,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面
`cordis:group``cordis:include` 一并注册,使一份组装能把一个提供方与它的消费方放进同一个 `isolate` realm。两者都通过宿主的模块管线加载而非被包含树自身的说明符解析这正是让本工作区之外的组装——放在 Harness home 下的 agent preset——能够使用 group 行的原因。
配置中的裸插件 specifier`@deepseek-ai/dsh-*`、npm 包)通过 Cordis Loader 的内部模块 loader 解析。仓库 bin 会安装 Loader 的可选 peer `node-addon-require-builtin`;外部调用方必须提供该组件,或者把插件安装到普通 Node import 解析可以找到的位置。相对 specifier 无需原生 helper并以配置目录为基准解析。构建后的 `dsh-app-boot` 产物内嵌静态挂载的 Include 实现,但仍将 Loader 保持为外部依赖,因此 include 树与宿主会绑定到同一个 Loader peer。`dsh` 源码启动器还会将 manifest元数据清单声明的 workspace 包映射到其 TypeScript 源码其配置门禁要求每个随附的原始Web 裸插件都出现在解析所用 manifest 的 `dependencies` 中。
配置中的裸插件 specifier`@deepseek-ai/dsh-*`、npm 包)通过 Cordis Loader 的内部模块 loader 解析。默认情况下,它们从配置目录解析;封闭运行时会向 `boot``mountRootInclude` 传入 `bareModuleBaseUrl`,使已安装包树保持权威,即使配置位于另一个 Node 项目中也不受遮蔽。相对 specifier 始终以配置目录为基准解析。仓库 bin 会安装 Loader 的可选 peer `node-addon-require-builtin`;外部调用方必须提供该组件,或者把插件安装到普通 Node import 解析可以找到的位置。构建后的 `dsh-app-boot` 产物内嵌静态挂载的 Include 实现,但仍将 Loader 保持为外部依赖,因此 include 树与宿主会绑定到同一个 Loader peer。`pnpm dsh` 源码路径还会将 manifest元数据清单声明的 workspace 包映射到其 TypeScript 源码其配置门禁要求每个随附的原始Web 裸插件都出现在解析所用 manifest 的 `dependencies` 中。
此包不包含 loader 钩子,也不提供开发模式接口。[`dsh` 应用](../../../apps/cli/README.md) 持有自己的 Node 源码启动钩子,并在启动序列中使用这些 helper构建后的消费方仍使用普通 Node 包解析。
@@ -42,7 +42,7 @@ profile 是位于 `$DSH_HOME/profiles/<name>` 下的目录Harness home 由 [`
- **`.env`**:产品 CLI 的普通环境层;调用目录的文件优先于 Harness home 的文件,两者都低于继承环境。`loadLayeredEnv` 记录每个值的来源,按不区分大小写的方式拒绝 [bootstrap-only 文件变量](../../../.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md#decision),并把其余值物化进 `process.env`,供 Loader 表达式和第三方库使用。受管凭据另存于 [`.credentials.yaml`](../../credentials/credentials-local/README.md);留在任一 `.env` 中的凭据仍是低优先级后备值。
- **`cordis.patch.yml`**home 级)与 **`profiles/<name>/cordis.patch.yml`**:用户 patch 层,应用在所有组合包层之后(先应用逐 profile 的文件,再应用 home 级文件,因此后者优先级更高):按 id 定位的 patch 会替换对应条目的整个 `config`(未改字段也要重述),`insert` 会添加条目,`!!js` 表达式则在挂载时插值。如果 patch 指定的条目 id 不在组合后的树中,则输出一条 stderr 警告。空文件或仅含注释的文件会抛出异常(其解析结果为空,而不是列表);如需禁用该层,请使用 `[]`
长期运行的界面会持续应用 `cordis.patch.yml` 的变更,具体由 `watchUserPatches` 负责;一次性运行只读取启动时的值。即使该文件或其直接父目录不存在,监视器仍会监视确切路径;它会串行处理突发变更,并按调用方的层次顺序重新组合用户 patch组合包层在下、overlay/标志 patch 在上)。读取失败、解析失败或 Loader 候选被拒时最后一个可用树会继续运行HMR 服务记录错误后广播 `hmr/config-update-failed(filename, Error)`,并隔离观察方的失败。上下文 dispose 时会关闭 watcher并等待进行中的刷新结束。
长期运行的界面会持续应用 `cordis.patch.yml` 的变更,具体由 `watchUserPatches` 负责;一次性运行只读取启动时的值。即使该文件或其直接父目录不存在,监视器仍会监视确切路径;它会串行处理突发变更,并按调用方的层次顺序重新组合用户 patch组合包层在下、overlay 在上)。读取失败、解析失败或 Loader 候选被拒时最后一个可用树会继续运行HMR 服务记录错误后广播 `hmr/config-update-failed(filename, Error)`,并隔离观察方的失败。上下文 dispose 时会关闭 watcher并等待进行中的刷新结束。
## 模型体验

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-app-boot",
"description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/boot/app-boot"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -28,32 +35,32 @@
"js-yaml": "^4.2.0"
},
"peerDependencies": {
"@cordisjs/plugin-group": "^1.0.0",
"@cordisjs/plugin-hmr": "^1.0.15",
"@cordisjs/plugin-include": "^1.0.4",
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-environment": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis-plugin-group": "workspace:^",
"@deepseek-ai/cordis-plugin-hmr": "workspace:^",
"@deepseek-ai/cordis-plugin-include": "workspace:^",
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"peerDependenciesMeta": {
"@cordisjs/plugin-hmr": {
"@deepseek-ai/cordis-plugin-hmr": {
"optional": true
}
},
"devDependencies": {
"@cordisjs/plugin-group": "workspace:^",
"@cordisjs/plugin-hmr": "workspace:^",
"@cordisjs/plugin-include": "workspace:^",
"@cordisjs/plugin-loader": "workspace:^",
"@cordisjs/plugin-timer": "workspace:^",
"@deepseek-ai/cordis-plugin-group": "workspace:^",
"@deepseek-ai/cordis-plugin-hmr": "workspace:^",
"@deepseek-ai/cordis-plugin-include": "workspace:^",
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/cordis-plugin-timer": "workspace:^",
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@types/js-yaml": "^4.0.9",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -9,19 +9,19 @@
import { pathToFileURL } from 'node:url'
import { readFileSync } from 'node:fs'
import { parseEnv } from 'node:util'
import { basename, dirname, resolve } from 'node:path'
import { basename, dirname, isAbsolute, resolve } from 'node:path'
import * as yaml from 'js-yaml'
import { Context, type FiberState } from 'cordis'
import Loader, { type Entry, type EntryOptions } from '@cordisjs/plugin-loader'
import Include, { applyEntryPatches, entryListSchema, type PatchOptions } from '@cordisjs/plugin-include'
import Group from '@cordisjs/plugin-group'
import { Context, type FiberState } from '@deepseek-ai/cordis'
import Loader, { type Entry, type EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
import Include, { applyEntryPatches, entryListSchema, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import Group from '@deepseek-ai/cordis-plugin-group'
import { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-paths'
import { createEnvironmentSnapshot, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
import type {} from '@cordisjs/plugin-hmr'
import type {} from '@deepseek-ai/cordis-plugin-hmr'
// Side-effect type import: resolves `ctx.get('systemPrompt')` to the service.
import type {} from '@deepseek-ai/dsh-system-prompt'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
/** Harness-home path resolver available to Loader `!!js` config expressions. */
dshHomePath?: typeof dshHomePath
@@ -199,7 +199,7 @@ export function loadLayeredEnv(
const bootstrapIncludes = new WeakMap<Context, Entry>()
// The include's YAML dialect (`!!js` scalars become expression nodes the
// Loader interpolates against each entry's context at mount time), imported
// Loader interpolates against each entry's injection-ready context), imported
// from the include itself so patch parsing and config dumping can never drift
// from what the include mounts. User patch layers share it so they may
// reference `process.env`.
@@ -215,7 +215,7 @@ export interface UserPatchWatchOptions {
* Compose the full patch list for a fresh user-layer generation —
* the same composition the app booted with, so a reload can interleave the
* new user patches between app-owned layers (bundle layers below,
* overlay/flag patches above). Identity when omitted: the user layer
* overlays above). Identity when omitted: the user layer
* is the whole patch list.
*/
compose?: (userPatches: PatchOptions[]) => PatchOptions[]
@@ -265,7 +265,7 @@ export async function watchUserPatches(
/**
* Load an optional patch-list file: a top-level YAML array of loader patch
* entries (`@cordisjs/plugin-include`'s `PatchOptions`): id-targeted config
* entries (`@deepseek-ai/cordis-plugin-include`'s `PatchOptions`): id-targeted config
* overrides and `insert` lists, with `!!js` expressions allowed. A missing
* file means "no layer"; an unreadable, unparsable, or non-array file throws —
* a present patch file that cannot apply is a misconfiguration and must fail
@@ -305,7 +305,7 @@ export function loadOverlayPatches(binName: string, file: string): PatchOptions[
}
/**
* Parse one loader patch list: a top-level YAML array of
* `@cordisjs/plugin-include` `PatchOptions` (id-targeted config overrides and
* `@deepseek-ai/cordis-plugin-include` `PatchOptions` (id-targeted config overrides and
* `insert` lists, `!!js` expressions allowed). Every invalid field or value throws,
* because a patch file that cannot be applied at all is a misconfiguration; a
* single patch whose target row is absent stays a per-entry Loader warning, so
@@ -476,6 +476,8 @@ function groupedDump(
* @param ctx - context carrying an initialized Loader service.
* @param absoluteConfigPath - absolute YAML or JSON configuration path.
* @param patches - initial app and user patches, applied in order.
* @param bareModuleBaseUrl - optional installed-host base for bare package
* names; relative names continue to resolve beside the configuration file.
* @returns the created root Include entry, or `undefined` when a surface
* disposed the whole tree (taking the Loader service with it) while the
* transactional create was still settling entry lifecycle.
@@ -484,24 +486,38 @@ export async function mountRootInclude(
ctx: Context,
absoluteConfigPath: string,
patches: readonly PatchOptions[] = [],
bareModuleBaseUrl?: string,
): Promise<Entry | undefined> {
ctx.loader.builtins.include = Include
ctx.loader.builtins.include = bareModuleBaseUrl === undefined
? Include
: class HostResolvedRootInclude extends Include {
override import(name: string, getOuterStack?: () => string[]): unknown {
const specifier = isAbsolute(name) ? pathToFileURL(name).href : name
if (name.startsWith('.') || name.startsWith('cordis:')) return super.import(specifier, getOuterStack)
const internal = this.ctx.loader.internal
/* v8 ignore next -- Node supplies the internal loader; this preserves the
original diagnostic for hypothetical embedders without it. */
if (internal === undefined) return super.import(specifier, getOuterStack)
return internal.import(specifier, bareModuleBaseUrl, {})
}
}
// `cordis:group` alongside it: a group row is how a composition gives one
// `isolate` realm to a provider and its consumers together, and an agent
// preset living outside this workspace cannot resolve `@cordisjs/plugin-group`
// preset living outside this workspace cannot resolve `@deepseek-ai/cordis-plugin-group`
// by name. Both builtins load through the ambient module pipeline, so neither
// depends on the included tree's own specifier resolution.
ctx.loader.builtins.group = Group
// Pinned id: the bootstrap include is app glue, not a config row, and its
// id appears in Loader failure chains — a random id would make startup
// diagnostics unstable across runs (and snapshot fixtures).
const includeConfig: Include.Config = {
path: pathToFileURL(absoluteConfigPath).href,
...patches.length > 0 ? { patches: [...patches] } : {},
}
const rootInclude: EntryOptions = {
id: 'include',
name: 'cordis:include',
config: {
path: pathToFileURL(absoluteConfigPath).href,
...patches.length > 0 ? { patches: [...patches] } : {},
},
config: includeConfig,
}
const includeId = await ctx.loader.create(rootInclude)
const loader = ctx.get('loader')
@@ -709,14 +725,13 @@ export async function assertEntriesActivated(ctx: Context, binName: string): Pro
/**
* Boot the Loader against `absoluteConfigPath` and return only after the whole
* tree settles. Entry names load through the Loader's internal module loader
* against `baseUrl` (the config directory), which may live outside
* `node_modules` reach and, unbuilt, cannot load vendored source; the
* bootstrap include is therefore statically imported and mounted as the
* `cordis:include` builtin, loading through the ambient module pipeline
* (vite/tsx/plain ESM) while the included tree's own specifiers stay
* config-relative. The package build embeds Include while leaving Loader
* external, so the built include tree and host share one Loader peer. Loader
* tree settles. Relative entry names resolve against the config directory;
* bare package names resolve there by default or against an explicit
* `bareModuleBaseUrl` for closed packaged runtimes. The bootstrap include
* is statically imported and mounted as the `cordis:include` builtin, loading
* through the ambient module pipeline (vite/tsx/plain ESM). The package build
* embeds Include while leaving Loader external, so the built include tree and
* host share one Loader peer. Loader
* settlement rejects startup failures, which `boot` wraps after disposing the
* partial context; a missing fiber or never-activating entry is rejected by
* the final audit, {@link assertEntriesActivated}, which rethrows a plugin's
@@ -729,6 +744,9 @@ export async function assertEntriesActivated(ctx: Context, binName: string): Pro
* @param patches - optional overlay patches applied over the included tree
* (see {@link loadOptionalPatches}); an empty list mounts none.
* @param prepare - optional host setup run after Loader installation and before any config-tree entry mounts.
* @param bareModuleBaseUrl - optional installed-host base for bare package
* names; use it when the host, rather than the configuration project, owns the
* complete plugin set.
* @returns the root context once every entry has started, or as soon as a
* surface disposed the tree while startup was still in flight.
* @throws a labelled error after disposing the partial context — `host
@@ -740,6 +758,7 @@ export async function boot(
absoluteConfigPath: string,
patches?: PatchOptions[],
prepare?: (ctx: Context) => Promise<void> | void,
bareModuleBaseUrl?: string,
): Promise<Context> {
const ctx = new Context()
// Two failure labels: `prepare` runs before any config-tree entry mounts,
@@ -751,7 +770,7 @@ export async function boot(
await ctx.plugin(Loader)
await prepare?.(ctx)
stage = 'plugin tree failed to load'
await mountRootInclude(ctx, absoluteConfigPath, patches)
await mountRootInclude(ctx, absoluteConfigPath, patches, bareModuleBaseUrl)
// A surface can finish and dispose the whole tree while startup is still
// in flight, before the last entry settles. The Loader service goes with
// it, and the activation audit describes a live tree — reading `ctx.loader`

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-app-boot'

View File

@@ -27,8 +27,8 @@ import {
existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync,
} from 'node:fs'
import { basename, dirname, join } from 'node:path'
import type { EntryOptions } from '@cordisjs/plugin-loader'
import { applyEntryPatches, type PatchOptions } from '@cordisjs/plugin-include'
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
import { loadOverlayPatches } from './index.ts'

View File

@@ -1,8 +1,9 @@
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve, sep } from 'node:path'
import { pathToFileURL } from 'node:url'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import {
addHarnessSourceSection, assertEntriesActivated, assertEntriesLoaded, boot,
@@ -557,6 +558,73 @@ describe('boot', () => {
}
})
it('can resolve bare plugins from the harness when the config project shadows their package name', async () => {
const dir = tmp()
const harness = tmp()
const absolutePlugin = join(dir, 'absolute.mjs')
const shadow = join(dir, 'node_modules', '@deepseek-ai', 'dsh-system-prompt')
const harnessPlugin = join(harness, 'node_modules', '@deepseek-ai', 'dsh-system-prompt')
mkdirSync(shadow, { recursive: true })
mkdirSync(harnessPlugin, { recursive: true })
writeFileSync(join(shadow, 'package.json'), JSON.stringify({
name: '@deepseek-ai/dsh-system-prompt',
type: 'module',
exports: './index.mjs',
}))
writeFileSync(join(shadow, 'index.mjs'), [
'export function apply(ctx) {',
' ctx.provide("shadowPluginLoaded", true)',
'}',
'',
].join('\n'))
writeFileSync(join(harnessPlugin, 'package.json'), JSON.stringify({
name: '@deepseek-ai/dsh-system-prompt',
type: 'module',
exports: './index.mjs',
}))
writeFileSync(join(harnessPlugin, 'index.mjs'), [
'export function apply(ctx) {',
' ctx.provide("harnessPluginLoaded", true)',
'}',
'',
].join('\n'))
writeFileSync(join(dir, 'relative.mjs'), 'export function apply(ctx) { ctx.provide("relativePluginLoaded", true) }\n')
writeFileSync(absolutePlugin, 'export function apply(ctx) { ctx.provide("absolutePluginLoaded", true) }\n')
const entries = [
'- id: prompt',
" name: '@deepseek-ai/dsh-system-prompt'",
'- id: relative',
" name: './relative.mjs'",
]
const configOwnedPath = join(dir, 'config-owned.cordis.yml')
writeFileSync(configOwnedPath, [...entries, ''].join('\n'))
const hostOwnedPath = join(dir, 'host-owned.cordis.yml')
writeFileSync(hostOwnedPath, [
...entries,
'- id: absolute',
` name: ${JSON.stringify(absolutePlugin)}`,
'',
].join('\n'))
const configOwned = await boot(NAME, configOwnedPath)
try {
expect(configOwned.get('shadowPluginLoaded')).toBe(true)
expect(configOwned.get('systemPrompt')).toBeUndefined()
expect(configOwned.get('relativePluginLoaded')).toBe(true)
} finally {
await configOwned.fiber.dispose()
}
const harnessBaseUrl = pathToFileURL(join(harness, 'entry.mjs')).href
const ctx = await boot(NAME, hostOwnedPath, undefined, undefined, harnessBaseUrl)
try {
expect(ctx.get('harnessPluginLoaded')).toBe(true)
expect(ctx.get('shadowPluginLoaded')).toBeUndefined()
expect(ctx.get('relativePluginLoaded')).toBe(true)
expect(ctx.get('absolutePluginLoaded')).toBe(true)
} finally {
await ctx.fiber.dispose()
}
})
it('runs host preparation before the Loader tree mounts', async () => {
const dir = tmp()
writeFileSync(join(dir, 'noop.mjs'), 'export const name = "noop"\nexport function apply() {}\n')
@@ -631,7 +699,18 @@ describe('boot', () => {
'}',
'',
].join('\n'))
writeFileSync(join(dir, 'cordis.yml'), '- id: exiting\n name: ./exiting.mjs\n')
writeFileSync(join(dir, 'delayed.mjs'), [
'await new Promise(resolve => setTimeout(resolve, 10))',
'export function apply() {}',
'',
].join('\n'))
writeFileSync(join(dir, 'cordis.yml'), [
'- id: exiting',
' name: ./exiting.mjs',
'- id: delayed',
' name: ./delayed.mjs',
'',
].join('\n'))
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
expect(ctx.get('loader')).toBeUndefined()
})
@@ -644,6 +723,25 @@ describe('boot', () => {
)
})
it('labels a deferred config failure with its row and leaves the source file unchanged', async () => {
const dir = tmp()
const configPath = join(dir, 'cordis.yml')
const config = [
'- id: invalid-config',
' name: ./noop.mjs',
' config:',
' value: !!js "JSON.parse(\'invalid\')"',
'',
].join('\n')
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
writeFileSync(configPath, config)
await expect(boot(NAME, configPath)).rejects.toThrow(
'failed to apply loader entry invalid-config (./noop.mjs)',
)
expect(readFileSync(configPath, 'utf8')).toBe(config)
})
it('appends the deepest cause with its original stack to the load failure', async () => {
const dir = tmp()
writeFileSync(join(dir, 'failing.mjs'), [

View File

@@ -12,7 +12,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import * as yaml from 'js-yaml'
import { entryListSchema } from '@cordisjs/plugin-include'
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
import { loadOverlayPatches, renderConfigDump } from '../src/index.ts'
const NAME = 'dsh-test-bin'

View File

@@ -8,8 +8,8 @@ import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import type { Include } from '@cordisjs/plugin-include'
import { Context } from '@deepseek-ai/cordis'
import type { Include } from '@deepseek-ai/cordis-plugin-include'
import { boot } from '../src/index.ts'
const NAME = 'dsh-test-bin'
@@ -391,7 +391,7 @@ describe('shipped builtins', () => {
it('lets a booted composition share one isolate realm across a group of rows', async () => {
// The reason `boot()` registers `cordis:group`: a composition — notably an
// agent preset living outside this workspace, which cannot resolve
// `@cordisjs/plugin-group` by name — gives a provider and its consumer one
// `@deepseek-ai/cordis-plugin-group` by name — gives a provider and its consumer one
// named realm so the service stays out of the root realm while remaining
// visible to the rows that need it.
const { ctx } = await bootTree([

View File

@@ -3,10 +3,10 @@ import { realpath } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { Context } from 'cordis'
import Hmr from '@cordisjs/plugin-hmr'
import Loader from '@cordisjs/plugin-loader'
import Timer from '@cordisjs/plugin-timer'
import { Context } from '@deepseek-ai/cordis'
import Hmr from '@deepseek-ai/cordis-plugin-hmr'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Timer from '@deepseek-ai/cordis-plugin-timer'
import { describe, expect, it, vi } from 'vitest'
async function bootHmr(dir: string, root: string[] = [], usePolling?: boolean): Promise<Context> {

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

@@ -9,10 +9,11 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import Hmr from '@cordisjs/plugin-hmr'
import Loader from '@cordisjs/plugin-loader'
import Timer from '@cordisjs/plugin-timer'
import { Context } from '@deepseek-ai/cordis'
import Hmr from '@deepseek-ai/cordis-plugin-hmr'
import Include, { type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Timer from '@deepseek-ai/cordis-plugin-timer'
import {
boot,
loadOptionalPatches,
@@ -92,23 +93,101 @@ describe('loadOptionalPatches', () => {
})
})
describe('boot with user patches', () => {
function writeTree(dir: string): string {
writeFileSync(join(dir, 'noop.mjs'), [
'export const name = "noop"',
'export function apply(_ctx, config = {}) {',
' if (config.fail) throw new Error("candidate config failed")',
'}',
function writeTree(dir: string): string {
writeFileSync(join(dir, 'noop.mjs'), [
'export const name = "noop"',
'export function apply(_ctx, config = {}) {',
' if (config.fail) throw new Error("candidate config failed")',
'}',
'',
].join('\n'))
writeFileSync(join(dir, 'cordis.yml'), '- id: noop\n name: ./noop.mjs\n config:\n value: base\n')
return join(dir, 'cordis.yml')
}
function entryConfig(ctx: Context, id: string): unknown {
return [...ctx.loader.entries()].find(entry => entry.options.id === id)?.options.config
}
describe('Loader config interpolation', () => {
it("resolves Include's own !!js options", async () => {
const dir = tmp()
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
writeFileSync(join(dir, 'cordis.yml'), '- id: noop\n name: ./noop.mjs\n')
const ctx = new Context()
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
ctx.provide('includePath', pathToFileURL(join(dir, 'cordis.yml')).href)
try {
await ctx.loader.create({
name: 'cordis:include',
config: { path: { __jsExpr: "ctx.get('includePath')" } },
})
await ctx.loader.await()
expect([...ctx.loader.entries()].some(entry => entry.options.id === 'noop')).toBe(true)
} finally {
await ctx.fiber.dispose()
}
})
it('waits for row injections before resolving !!js and resolves again after provider replacement', async () => {
const dir = tmp()
writeFileSync(join(dir, 'provider.mjs'), [
'export const name = "provider"',
'export function apply(ctx, config) { ctx.provide("phaseOne", config) }',
'',
].join('\n'))
writeFileSync(join(dir, 'cordis.yml'), '- id: noop\n name: ./noop.mjs\n config:\n value: base\n')
return join(dir, 'cordis.yml')
}
writeFileSync(join(dir, 'reader.mjs'), [
'export const name = "reader"',
'export const inject = ["phaseOne"]',
'export function apply(ctx, config) { ctx.provide("readerResult", config) }',
'',
].join('\n'))
writeFileSync(join(dir, 'cordis.yml'), '[]\n')
const composition: PatchOptions[] = [{
insert: [
{
// Consumer-first order proves interpolation follows injection
// readiness rather than YAML position.
id: 'reader',
name: './reader.mjs',
inject: ['phaseOne'],
config: { value: { __jsExpr: 'ctx.phaseOne.fail ? (() => { throw new Error("rejected provider") })() : ctx.phaseOne.value' } },
},
{ id: 'provider', name: './provider.mjs', config: { value: 'first' } },
],
}]
const ctx = await boot(NAME, join(dir, 'cordis.yml'), composition)
try {
expect(ctx.get('readerResult')).toEqual({ value: 'first' })
const provider = [...ctx.loader.entries()].find(entry => entry.options.id === 'provider')
expect(provider).toBeDefined()
await provider?.update({ disabled: true })
await ctx.loader.await()
expect(ctx.get('readerResult')).toBeUndefined()
await provider?.update({ config: { value: 'second' } })
await provider?.update({ disabled: false })
await ctx.loader.await()
expect(ctx.get('readerResult')).toEqual({ value: 'second' })
function entryConfig(ctx: Context, id: string): unknown {
return [...ctx.loader.entries()].find(entry => entry.options.id === id)?.options.config
}
await provider?.update({ disabled: true })
await provider?.update({ config: { fail: true } })
await provider?.update({ disabled: false })
await expect(ctx.loader.await()).rejects.toThrow('rejected provider')
expect(ctx.get('readerResult')).toBeUndefined()
await provider?.update({ disabled: true })
await provider?.update({ config: { value: 'recovered' } })
await provider?.update({ disabled: false })
await ctx.loader.await()
expect(ctx.get('readerResult')).toEqual({ value: 'recovered' })
} finally {
await ctx.fiber.dispose()
}
})
})
describe('boot with user patches', () => {
it('applies id-targeted overrides, inserts, and interpolates !!js from the environment', async () => {
const dir = tmp()
const userDir = tmp()

View File

@@ -14,6 +14,6 @@ export default defineConfig({
dts: false,
clean: false,
deps: {
alwaysBundle: ['@cordisjs/plugin-include'],
alwaysBundle: ['@deepseek-ai/cordis-plugin-include'],
},
})

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 packages/experimental/README.md
README.md: db39af8bb1b1bcfd257e16e4ad1dd112f604ffb1
README.zh.md: fc5942190a354668164b41b99e83b52b14d88f18
# pnpm run verify-translation-pairing --write packages/boot/cmdline/README.md
README.md: 98335e901bdf8fe33e14c1ad4c1a320d77f30c96
README.zh.md: 28ea749943c60089c6b4725cb61e121f82aa0114

View File

@@ -0,0 +1,74 @@
# `@deepseek-ai/dsh-cmdline`
English | [中文](README.zh.md)
The command line a dsh launcher hands to the app it boots. The launcher parses only its own flags (`--profile`, `--patch`, the config dumps) and hands **everything after them** to the tree verbatim, so an app owns its flag family, its `--help` text, and its parse errors instead of the launcher knowing them.
## The launcher values
A launcher calls `provideCmdline(ctx, host)` before any tree entry mounts, which provides:
- `ctx.cmdlineArgs` — the invocation's inner arguments. `get()` is the whole interface, and it returns a snapshot: `dsh --profile tui --resume abc` yields `['--resume', 'abc']`.
- `ctx.appExit` — a bounded process-exit request, wired to the launcher's shutdown controller.
An embedding host with no command line provides an empty list; that is the honest answer, not a missing value.
## Ordinary providers and injected config
Any app plugin may inject `cmdlineArgs`, parse it, and publish an ordinary app-owned service. `parseCmdline(ctx, program, plan)` is only a commander adapter; the caller owns the returned value and service:
```ts ignore
export const name = 'web-startup'
export const inject = ['cmdlineArgs']
export function apply(ctx: Context): void {
const values = parseCmdline(ctx, webCommand(), planWebStartup)
if (values !== undefined) ctx.provide('webStartup', values)
}
```
Its Loader row carries no launcher marker or special kind:
```yaml
- id: web-startup
name: '@deepseek-ai/dsh-web-app/startup'
```
Every row configured from those values uses ordinary service injection and direct lazy config access:
```yaml
- id: webserver
name: '@deepseek-ai/dsh-host-webserver'
inject: [webStartup]
config:
host: !!js ctx.webStartup.host ?? '127.0.0.1'
port: !!js ctx.webStartup.port ?? 3080
```
`parseCmdline` parses the immutable arguments and asks `plan` for the app-owned value. On `--help`, `--version`, a parse error, or a `program.error(...)` from the plan, it writes commander's text, requests exit, and returns `undefined`; the provider publishes nothing, so dependent rows never activate.
### How injection orders config
Loader defers a row's `!!js` interpolation until that row's declared injections are active, then evaluates against the row's plugin context. The example above can therefore read `ctx.webStartup` directly: Cordis has already populated that injected service before Loader asks for `webserver`'s config. Include trees preserve nested expression nodes until each target row reaches this point. Provider replacement and live patch reload repeat interpolation against the current injected services, so a launch flag cannot be silently reset.
`enableRow(ctx, id)` turns on a row a bundle ships disabled because only some invocations want it (`dsh web --dev` and its client-plugin reload chain). The activation is an in-memory override: it does not rewrite the row's configured `disabled` value and survives config reapplication for that mounted entry. Loader applies the enabled row's ordinary injection ordering.
### Shared immutable arguments
`get()` does not consume or mutate argv. Multiple plugins can parse the same snapshot and independently provide services. The launcher does not inspect the composition for a command-line owner; a profile with no reader simply ignores its app arguments.
An out-of-tree plugin brings its own commander copy, so commander's control-flow errors are detected structurally rather than by class identity; an identity check would rethrow a printed help as a fatal load failure.
## Model Experience
None, as this package resolves the process's own command line before any session exists.
#### KV Cache effect
None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **Launcher flags must precede app arguments.** The split is positional: the first token the launcher does not recognize starts the inner arguments, so `--patch` placed after an app flag belongs to the app. The launcher's parser consumes one `--`, so an app argument that must survive as a literal `--` needs `-- --`.
- **An app-owned service has no statically declared provider.** Consumer rows name it through ordinary injection; a bundle that omits its provider fails at settlement with pending entries naming the service rather than at load.
- **A user patch that replaces a row's whole `config` drops its expressions.** A flag beats the value written beside it, not a literal a user wrote in place of the expression; keeping the expression is what keeps the flag winning.

View File

@@ -0,0 +1,74 @@
# `@deepseek-ai/dsh-cmdline`
[English](README.md) | 中文
dsh 启动器交给它所引导应用的那条命令行。启动器只解析属于自己的 flag`--profile``--patch`、配置 dump并把**其后的一切**原样交给配置树,因此 flag 家族、`--help` 文本和解析错误都由应用自己持有,启动器不必知道它们。
## 启动器提供的值
启动器在任何配置树条目挂载之前调用 `provideCmdline(ctx, host)`,它提供:
- `ctx.cmdlineArgs`:本次调用的内层参数。`get()` 就是它的全部接口,返回一份快照:`dsh --profile tui --resume abc` 得到 `['--resume', 'abc']`
- `ctx.appExit`:一个有边界的进程退出请求,接到启动器的关停控制器上。
没有命令行的嵌入宿主提供空列表;这是诚实的答案,而不是缺失的值。
## 普通提供方与注入配置
任何应用插件都可以注入 `cmdlineArgs`、解析它,再发布一个普通的应用自有服务。`parseCmdline(ctx, program, plan)` 只适配 commander返回值与服务都归调用方持有
```ts ignore
export const name = 'web-startup'
export const inject = ['cmdlineArgs']
export function apply(ctx: Context): void {
const values = parseCmdline(ctx, webCommand(), planWebStartup)
if (values !== undefined) ctx.provide('webStartup', values)
}
```
它的 Loader 行不携带启动器标记,也没有特殊类型:
```yaml
- id: web-startup
name: '@deepseek-ai/dsh-web-app/startup'
```
所有由这些取值配置的行都使用普通服务注入,并在惰性配置中直接访问该服务:
```yaml
- id: webserver
name: '@deepseek-ai/dsh-host-webserver'
inject: [webStartup]
config:
host: !!js ctx.webStartup.host ?? '127.0.0.1'
port: !!js ctx.webStartup.port ?? 3080
```
`parseCmdline` 解析不可变参数,再向 `plan` 索取应用自有取值。遇到 `--help`、`--version`、解析错误,或 `plan` 发出的 `program.error(...)` 时,它输出 commander 文本、请求退出并返回 `undefined`;提供方什么也不发布,因此依赖行不会激活。
### 注入如何排列配置求值
Loader 会把一行的 `!!js` 插值推迟到该行声明的注入全部激活之后,再基于该行的插件上下文求值。所以上例可以直接读取 `ctx.webStartup`Loader 索取 `webserver` 的配置之前Cordis 已经填入了这个注入服务。Include 树会保留嵌套表达式节点,直到各个目标行到达这一时点。提供方替换与活动 patch 重载都会针对当前注入服务重新插值,因此启动 flag 不会被悄悄重置。
`enableRow(ctx, id)` 打开某个组合包以禁用状态交付、只有部分调用才需要的行(`dsh web --dev` 及其客户端插件重载链路)。该激活是内存中的覆盖:它不会改写行所配置的 `disabled` 值并会在已挂载条目的配置重新应用后继续生效。Loader 会对启用后的行应用普通的注入顺序。
### 共享不可变参数
`get()` 不会消费或修改 argv。多个插件可以解析同一份快照并分别提供服务。启动器不会检查组合中的命令行所有者没有读取方的 profile 只会忽略自己的应用参数。
树外插件会带来自己的一份 commander 副本,因此 commander 的控制流错误按结构识别,而不是按类身份识别;按身份判断会把已经打印出来的 help 重新抛成致命的加载失败。
## 模型体验
无。本包在任何会话存在之前解析进程自身的命令行。
#### KV Cache 影响
无;本包既不组装也不发送提供方请求。
## 已知限制与延期工作
- **启动器的 flag 必须写在应用参数之前**:切分按位置进行,启动器不认识的第一个 token 就是内层参数的起点,因此写在某个应用 flag 之后的 `--patch` 属于应用。启动器的解析器会消耗掉一个 `--`,因此必须以字面量 `--` 存活到应用的参数需要写成 `-- --`。
- **应用自有服务没有静态声明的提供方**:消费行通过普通注入点名它;缺少提供方的组合包会在结算时失败,由待处理条目点名该服务,而不是在加载时失败。
- **用户 patch 若整体替换某行的 `config`,会连同其中的表达式一起丢掉**flag 胜过的是表达式旁写着的那个值,而不是用户用字面量替换掉表达式之后的结果;保留表达式才能保留 flag 的优先级。

View File

@@ -0,0 +1,46 @@
{
"name": "@deepseek-ai/dsh-cmdline",
"description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/boot/cmdline"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/cordis-plugin-include": "workspace:^",
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"commander": "^15.0.0",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -0,0 +1,174 @@
/**
* @deepseek-ai/dsh-cmdline — the command line a dsh launcher hands to the app
* it boots.
*
* The launcher parses only its own flags (`--profile`, `--patch`, the config
* dumps) and hands everything after them to the tree verbatim through the
* {@link CmdlineArgs} service, so an app owns its flag family, its `--help`
* text, and its parse errors instead of the launcher knowing them.
*
* Any app plugin can inject `cmdlineArgs` and call {@link parseCmdline}. A
* provider may publish the parsed values as its own service, and ordinary rows
* can inject that service and read it from lazily resolved config —
* `port: !!js ctx.webStartup.port ?? 3080` — so a flag beats the value written
* beside it. No row has launcher-level command-line status.
* @module @deepseek-ai/dsh-cmdline
*/
import type { Command } from 'commander'
import type { Context } from '@deepseek-ai/cordis'
// Empty type import carries the Loader Context merge used by enableRow.
import type {} from '@deepseek-ai/cordis-plugin-loader'
/**
* The invocation's inner arguments: everything after the launcher's own flags,
* verbatim and in argv order. `dsh --profile tui --resume abc` yields
* `['--resume', 'abc']`.
*/
export interface CmdlineArgs {
/**
* Read the inner arguments.
* @returns the arguments in argv order; empty when the invocation carried none.
*/
get(): readonly string[]
}
/** Request bounded process exit; the launcher wires it to its shutdown controller. */
export interface AppExit {
/**
* Request exit once the tree has been disposed.
* @param code - the process exit code.
*/
(code: number): void
}
declare module '@deepseek-ai/cordis' {
interface Context {
/** The invocation's inner arguments; provided by a launcher before the tree mounts. */
cmdlineArgs?: CmdlineArgs
/** Bounded process-exit request; provided by a launcher before the tree mounts. */
appExit?: AppExit
}
}
/** The launcher facts an app needs. */
export interface CmdlineHost {
/** The invocation's inner arguments, in argv order. */
args: readonly string[]
/** Bounded process-exit request. */
exit: AppExit
}
/**
* Provide the command line and the exit request on a host context before any
* tree entry mounts. Both are launcher facts, not config: an embedding host
* with no command line provides an empty argument list.
* @param ctx - the host context the tree will mount under.
* @param host - the invocation's arguments and its exit request.
*/
export function provideCmdline(ctx: Context, host: CmdlineHost): void {
const snapshot: readonly string[] = Object.freeze([...host.args])
ctx.provide('cmdlineArgs', { get: () => snapshot })
ctx.provide('appExit', host.exit)
}
/** The process streams commander output is written to; production writes to the process. */
export const internals: { stdout: { write(chunk: string): unknown }; stderr: { write(chunk: string): unknown } } = {
stdout: process.stdout,
stderr: process.stderr,
}
/**
* Resolve parsed arguments into an app-owned value. Call
* `program.error(...)` to reject the invocation with a usage message instead
* of throwing.
* @param program - the parsed commander program.
* @param ctx - the plugin context that received the command line.
* @returns the value an ordinary provider plugin may publish.
*/
export type CmdlinePlan<T = unknown> = (program: Command, ctx: Context) => T
/**
* Parse the launcher's immutable argument snapshot with an app's commander
* program. The caller decides whether and how to publish the returned value;
* this helper has no Loader-row or service ownership semantics.
*
* Help, version, and rejected arguments are terminal for the process: commander
* writes the text, the helper requests `ctx.appExit`, and it returns
* `undefined` so the caller publishes nothing.
* @param ctx - plugin context carrying `cmdlineArgs` and `appExit`.
* @param program - the app's commander program, with its flags and description already declared.
* @param plan - this invocation's resolved value; omitted returns an empty object.
* @returns the resolved value, or `undefined` when the app asked to exit.
* @throws when the launcher did not provide the command line and exit request.
*/
export function parseCmdline<T>(
ctx: Context,
program: Command,
plan: CmdlinePlan<T> = (() => ({}) as T),
): T | undefined {
// Read through the global service store, not the property proxy: appExit is
// an optional host value and the plugin only needs to inject cmdlineArgs.
const args = ctx.get('cmdlineArgs')
const exit = ctx.get('appExit')
if (args === undefined || exit === undefined) {
throw new Error(`${program.name()}: the launcher must provide ctx.cmdlineArgs and ctx.appExit before the tree mounts`)
}
program
.exitOverride()
.configureOutput({
writeOut: text => void internals.stdout.write(text),
writeErr: text => void internals.stderr.write(text),
})
try {
program.parse(args.get(), { from: 'user' })
return plan(program, ctx)
} catch (error) {
// exitOverride turns help, version, a parse error, and a plan's own
// program.error() into a CommanderError; commander has already written the
// text through the output configured above.
if (!isCommanderError(error)) throw error
exit(error.exitCode)
return undefined
}
}
/**
* Turn on a row this composition ships disabled, because this invocation asked
* for it (`dsh web --dev` and its client-plugin reload chain).
*
* A row cannot be inserted from inside a mounting plugin — the Loader returns a
* prefixed id it then fails to resolve — so a conditional row ships disabled
* and a row mounted beside it enables it after startup resolves the invocation.
* The Loader keeps that activation in memory, separate from serialized options,
* so reapplying the composition cannot restore the invocation's row to disabled.
* @param ctx - plugin context whose Loader tree carries the row.
* @param id - the row id.
* @returns nothing once the row has started or is waiting for its dependencies.
* @throws when the Loader or named row is absent.
*/
export async function enableRow(ctx: Context, id: string): Promise<void> {
const loader = ctx.get('loader')
if (loader === undefined) throw new Error('dsh-cmdline: enabling a row requires the Loader service')
const entry = [...loader.entries()].find(candidate => candidate.options.id === id)
if (entry === undefined) throw new Error(`dsh-cmdline: the composition has no ${JSON.stringify(id)} row to enable`)
await entry.enableRuntime()
}
/**
* Whether a thrown value is commander's own control-flow error (help, version,
* a parse error, or `program.error`).
*
* Detected structurally, not with `instanceof`: an out-of-tree plugin brings
* its own commander copy, whose `CommanderError` class is a different identity
* from this package's, and an identity check there would rethrow a printed
* help as a fatal load failure.
* @param error - the thrown value.
* @returns true when the value carries commander's error code and exit code.
*/
function isCommanderError(error: unknown): error is { code: string; exitCode: number } {
if (typeof error !== 'object' || error === null) return false
const candidate = error as { code?: unknown; exitCode?: unknown }
return typeof candidate.code === 'string' && candidate.code.startsWith('commander.')
&& typeof candidate.exitCode === 'number'
}

View File

@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-cmdline`.
* @module @deepseek-ai/dsh-cmdline/invariant
*/
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-cmdline'
/** Cordis companion plugin name. */
export const name = 'cmdline-invariant'
/** Service required before the companion can register. */
export const inject = ['invariants']
/**
* No runtime invariant: `cmdlineArgs` is an immutable launcher fact that any
* number of ordinary plugins may read. App-owned providers and consumers use
* normal Cordis service injection, whose missing dependencies are already
* reported by Loader settlement.
*/
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))

View File

@@ -0,0 +1,265 @@
/**
* The launcher-to-app command line over a REAL Loader tree, mounted the way a
* profile boot mounts it: Loader holds each row until its injections are
* active, then resolves that row's config against its injection-ready context.
*/
import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { Command } from 'commander'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { afterEach, describe, expect, it } from 'vitest'
import {
enableRow, internals, parseCmdline, provideCmdline, type CmdlinePlan,
} from '../src/index.ts'
/** Every value one boot of the fixture tree observed. */
interface Observed {
/** Config the reading row started with; absent means it never started. */
started?: Record<string, unknown>
exits: number[]
out: string
}
/** A booted fixture tree: what it observed, and its root for direct parser calls. */
interface Fixture {
observed: Observed
ctx: Context
}
const disposers: (() => Promise<void>)[] = []
afterEach(async () => {
for (const dispose of disposers.splice(0)) await dispose()
internals.stdout = process.stdout
internals.stderr = process.stderr
})
/** The fixture app's flag family: one `--port` its rows read from the service. */
function demoCommand(): Command {
return new Command().name('demo').exitOverride().option('--port <port>', 'listen port')
}
/** The fixture app's plan: the resolved values its rows read. */
const demoPlan: CmdlinePlan<{ port?: number }> = (program) => {
const port = program.opts<{ port?: string }>().port
if (port === undefined) return {}
if (!/^\d+$/.test(port)) program.error(`error: --port must be a number, got ${JSON.stringify(port)}`)
return { port: Number(port) }
}
/** A YAML `!!js` expression node, as the include parses one out of a patch file. */
const expression = (source: string): unknown => ({ __jsExpr: source })
/**
* Mount a two-row composition the way a profile boot does: both rows at once,
* with Loader ordering config resolution from their injections.
* @param args - the invocation's inner arguments.
* @param plan - the app's plan; defaults to the fixture's own.
* @returns the booted fixture.
*/
async function bootFixture(
args: string[],
plan: CmdlinePlan = demoPlan,
options: { objectInject?: boolean; withoutProvider?: boolean } = {},
): Promise<Fixture> {
const dir = mkdtempSync(join(tmpdir(), 'dsh-cmdline-'))
const observed: Observed = { exits: [], out: '' }
writeFileSync(join(dir, 'reader.mjs'), `
export const name = 'reader'
export const inject = ['demoStartup']
export function apply(ctx, config) { globalThis.__observed.started = config }
`)
// The Loader imports a row through Node's own resolver, which cannot resolve
// this workspace's sources; the row delegates to the real function the test
// imported through the source-plane path mapping.
writeFileSync(join(dir, 'startup.mjs'), `
export const name = 'demo-startup'
export const inject = ['cmdlineArgs']
export function apply(ctx) { return globalThis.__provideDemoArgs(ctx) }
`)
writeFileSync(join(dir, 'cordis.yml'), '[]\n')
const observing = { write: (chunk: string) => { observed.out += chunk; return true } }
internals.stdout = observing
internals.stderr = observing
const globals = globalThis as unknown as { __observed: Observed; __provideDemoArgs: (ctx: Context) => void }
globals.__observed = observed
globals.__provideDemoArgs = (ctx: Context) => {
const values = parseCmdline(ctx, demoCommand(), plan)
if (values !== undefined) ctx.provide('demoStartup', values)
}
// The composition, exactly as a profile delivers one: include patches whose
// config carries `!!js` expressions.
const composition: PatchOptions[] = [{
insert: [
...options.withoutProvider === true
? []
: [{ id: 'demo-startup', name: pathToFileURL(join(dir, 'startup.mjs')).href }],
{
id: 'reader',
name: pathToFileURL(join(dir, 'reader.mjs')).href,
inject: options.objectInject === true ? { demoStartup: { required: true } } : ['demoStartup'],
config: { port: expression('ctx.demoStartup.port ?? 3080') },
},
],
}]
const ctx = new Context()
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
provideCmdline(ctx, { args, exit: code => void observed.exits.push(code) })
await ctx.loader.create({
name: 'cordis:include',
config: { path: pathToFileURL(join(dir, 'cordis.yml')).href, patches: structuredClone(composition) },
})
await ctx.loader.await()
disposers.push(async () => { await ctx.fiber.dispose() })
return { observed, ctx }
}
describe('parseCmdline', () => {
it('lets a row read the flag value the app resolved', async () => {
const { observed } = await bootFixture(['--port', '8080'])
expect(observed.started).toEqual({ port: 8080 })
expect(observed.exits).toEqual([])
})
it('leaves a row on the value written beside the expression when no flag names one', async () => {
const { observed } = await bootFixture([])
expect(observed.started).toEqual({ port: 3080 })
})
it('recognizes the Loader object form of a provider-service injection', async () => {
const { observed } = await bootFixture(['--port', '8080'], demoPlan, { objectInject: true })
expect(observed.started).toEqual({ port: 8080 })
})
it('prints the app help, starts no reading row, and requests exit 0', async () => {
const { observed } = await bootFixture(['--help'])
expect(observed.out).toContain('Usage: demo')
expect(observed.started).toBeUndefined()
expect(observed.exits).toEqual([0])
})
it('rejects the invocation from the plan without starting the app', async () => {
const { observed } = await bootFixture(['--port', 'abc'])
expect(observed.out).toContain('--port must be a number')
expect(observed.started).toBeUndefined()
expect(observed.exits).toEqual([1])
})
it('rethrows a plan failure that is not commander asking to exit', async () => {
const { ctx } = await bootFixture([], demoPlan, { withoutProvider: true })
const plan: CmdlinePlan = () => { throw new Error('plan exploded') }
expect(() => { parseCmdline(ctx, demoCommand(), plan) }).toThrow('plan exploded')
})
it('rethrows a thrown value that is not an object at all', async () => {
const { ctx } = await bootFixture([], demoPlan, { withoutProvider: true })
const plan: CmdlinePlan = () => {
const thrown: unknown = 'plan threw a string'
throw thrown
}
expect(() => { parseCmdline(ctx, demoCommand(), plan) }).toThrow('plan threw a string')
})
it('returns values without inspecting Loader rows or owning a service', async () => {
const { ctx } = await bootFixture([], demoPlan, { withoutProvider: true })
expect(parseCmdline(ctx, demoCommand())).toEqual({})
expect(ctx.get('demoStartup')).toBeUndefined()
})
})
describe('enableRow', () => {
it('enables the named Loader row and fails loud when the Loader or row is absent', async () => {
const withoutLoader = new Context()
await expect(enableRow(withoutLoader, 'client-hmr')).rejects.toThrow('requires the Loader service')
const ctx = new Context()
let enabled = false
ctx.provide('loader', {
entries: () => [{
options: { id: 'client-hmr' },
enableRuntime: async () => { enabled = true },
}],
} as never)
await enableRow(ctx, 'client-hmr')
expect(enabled).toBe(true)
await expect(enableRow(ctx, 'absent')).rejects.toThrow('no "absent" row to enable')
})
it('keeps invocation-only activation through config reapplication', async () => {
const dir = mkdtempSync(join(tmpdir(), 'dsh-runtime-enable-'))
const observed = { starts: 0, stops: 0 }
;(globalThis as unknown as { __runtimeEnableObserved: typeof observed }).__runtimeEnableObserved = observed
writeFileSync(join(dir, 'conditional.mjs'), `
export function apply(ctx) {
globalThis.__runtimeEnableObserved.starts += 1
ctx.effect(() => () => { globalThis.__runtimeEnableObserved.stops += 1 })
}
`)
writeFileSync(join(dir, 'cordis.yml'), [
'- id: conditional',
` name: ${pathToFileURL(join(dir, 'conditional.mjs')).href}`,
' disabled: true',
'',
].join('\n'))
const ctx = new Context()
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
await ctx.loader.create({
name: 'cordis:include',
config: { path: pathToFileURL(join(dir, 'cordis.yml')).href },
})
await ctx.loader.await()
const conditional = [...ctx.loader.entries()].find(entry => entry.options.id === 'conditional')
const include = [...ctx.loader.entries()].find(entry => entry.options.name === 'cordis:include')
expect(conditional).toBeDefined()
expect(include?.fiber).toBeDefined()
expect(conditional?.options.disabled).toBe(true)
expect(observed).toEqual({ starts: 0, stops: 0 })
await enableRow(ctx, 'conditional')
await ctx.loader.await()
expect(conditional?.disabled).toBe(false)
expect(conditional?.options.disabled).toBe(true)
expect(observed).toEqual({ starts: 1, stops: 0 })
await include!.fiber!.update(include!.options.config, true)
await ctx.loader.await()
expect(conditional?.disabled).toBe(false)
expect(conditional?.options.disabled).toBe(true)
expect(observed).toEqual({ starts: 1, stops: 0 })
disposers.push(async () => { await ctx.fiber.dispose() })
})
})
describe('provideCmdline', () => {
it('hands the app a snapshot the caller cannot mutate afterwards', () => {
const ctx = new Context()
const args = ['--resume', 'abc']
provideCmdline(ctx, { args, exit: () => {} })
args.push('--tampered')
expect(ctx.cmdlineArgs?.get()).toEqual(['--resume', 'abc'])
})
it('fails loud when a parser runs without the launcher values', () => {
const ctx = new Context()
expect(() => { parseCmdline(ctx, demoCommand()) })
.toThrow('the launcher must provide ctx.cmdlineArgs and ctx.appExit')
})
it('lets multiple parsers read the same immutable snapshot', () => {
const ctx = new Context()
provideCmdline(ctx, { args: ['--port', '8080'], exit: () => {} })
expect(parseCmdline(ctx, demoCommand(), demoPlan)).toEqual({ port: 8080 })
expect(parseCmdline(ctx, demoCommand(), demoPlan)).toEqual({ port: 8080 })
expect(Object.isFrozen(ctx.cmdlineArgs?.get())).toBe(true)
})
})

View File

@@ -8,24 +8,15 @@
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/include"
},
{
"path": "../../../vendor/loader"
},
{
"path": "../../skill/skill-local"
},
{
"path": "../../mcp/mcp-client"
},
{
"path": "../../util/paths"
},
{
"path": "../../support/invariants"
}

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