Merge remote-tracking branch 'origin/master' into dshw/pr-2423

# Conflicts:
#	packages/client/ui-sidebar/src/client/SidebarRoot.tsx
This commit is contained in:
_Kerman
2026-08-13 04:32:57 +08:00
3517 changed files with 51843 additions and 30957 deletions

View File

@@ -17,9 +17,9 @@ These package-specific rules supplement the repo-wide [conventions](../AGENTS.md
- **Registry contributions prove disposal** through the HMR-safety test required by [testing policy](../docs/testing.md): dispose the fiber and observe removal.
- **Every package owns `./invariant`.** Register the manifest name; check an event/data relation or give empty installers package-specific `No runtime invariant:` reasons. Generated companions, unexplained empties, and ignored reporters fail [`verify-package-invariants`](../.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md).
Naming notes:
[Naming rules](../docs/cookbook/adding-a-package.md#name-the-role-that-exists):
- **Package tsconfig:** extends `tsconfig.base.json` (Client: `tsconfig.base.client.json`), uses `rootDir: src`, `outDir: lib/types`, and references each workspace dependency plus `support/invariants`; registers in exactly one aggregate. Only `api/remotes` splits for generated contracts; ordinary two-entry Client plugins do not ([layout](../docs/development.md#typescript-project-layout)).
- **Package tsconfig:** extends `tsconfig.base.json` (Client: `tsconfig.base.client.json`), uses `rootDir: src`, `outDir: lib/types`, and references each workspace dependency plus `runtime-diagnostics/invariants`; registers in exactly one aggregate. Only `api/remotes` splits for generated contracts; ordinary two-entry Client plugins do not ([layout](../docs/development.md#typescript-project-layout)).
- `src/types.ts` contains only types — no runtime code.
- Tests live at package level under `tests/`, not `src/__tests__/`.
- A package's README and JSDoc are part of the change: altered behavior (config keys, defaults, error codes, wire fields) updates them in the same commit. `doc-sync` gates what it can; apply [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for complete, concise prose and verify accuracy against code.

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: 7a1fce6e361a47cf4ac6f02a76107e049411662e
README.zh.md: 8c96a59e06bb327d906650d9bae02486668d41e4
README.md: 2e28b190378ed8ca25102d6311bd8a2c8d5e1c58
README.zh.md: e0e1973d146c241d4846c4c237ed0e5f6a0d7b8a

View File

@@ -11,25 +11,26 @@ Groups hold `packages/<group>/<pkg>/`; names stay `@deepseek-ai/dsh-<pkg>`. **Gr
| Group | Role | Release expectation |
|---|---|---|
| [`core/`](core/README.md) | Product API spine: sessions, prompts, tools, agent services, and the concrete loop | Product — stable API |
| [`api/`](api/README.md) | Remote BFF assembly and TypeRT RPC gateway | Product — stable API |
| [`api/`](api/README.md) | Remote BFF assembly and Typert RPC gateway | Product — stable API |
| [`typert/`](typert/README.md) | Type graph generation, artifact loading, and runtime registry | Product — stable API |
| [`goal/`](goal/README.md) | Same-session goal persistence and lifecycle | Product — stable API |
| [`schedule/`](schedule/README.md) | Session-local scheduled follow-ups | Product — stable API |
| [`feedback/`](feedback/README.md) | Human feedback | Product — stable API |
| [`identity/`](identity/README.md) | Shared anonymous identity | Product — stable API |
| [`llm/`](llm/README.md) | LLM capability family: the abstract service + provider adapters | Product — stable API |
| [`e2b/`](e2b/README.md) | E2B providers | POC |
| [`subprocess/`](subprocess/README.md) | Subprocess capability family: Service Definition + local process-tree provider | Product — stable API |
| [`bash/`](bash/README.md) | Bash capability family: executor seam, local impl, model-facing tool | Product — stable API |
| [`pty/`](pty/README.md) | Persistent PTY capability family: owner-scoped sessions, local implementation, and model-facing tools | Product — stable API |
| [`shell/`](shell/README.md) | Bash capability family: executor seam, local impl, model-facing tool | Product — stable API |
| [`terminal/`](terminal/README.md) | Persistent PTY capability family: owner-scoped sessions, local implementation, and model-facing tools | Product — stable API |
| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: Service Definition + worker-thread provider + Code Mode Consumer | Product — stable API |
| [`sandbox/`](sandbox/README.md) | Process-confinement seam; bwrap/Landlock/Seatbelt backends | Product — stable API |
| [`fs/`](fs/README.md) | Filesystem capability family: seam, local impl, model-facing file tools, bash-backed discovery tools | Product — stable API |
| [`lsp/`](lsp/README.md) | LSP capability family: seam, generic stdio provider, and the `lsp` tool | Product — stable API |
| [`skill/`](skill/README.md) | Skill capability family: the provider registry, local provider, and model-facing catalog/loader | Product — stable API |
| [`compact/`](compact/README.md) | Compaction capability family: Service Definition + basic provider + command Consumer | Product — stable API |
| [`compaction/`](compaction/README.md) | Compaction capability family: Service Definition + basic provider + command Consumer | Product — stable API |
| [`context/`](context/README.md) | Model-visible request context, including workspace instructions and time context | Product — stable API |
| [`subagent/`](subagent/README.md) | Subagent capability family: the provider-registry contract and the model-facing delegation tool | Product — stable API |
| [`tasks/`](tasks/README.md) | Generic background-task runtime and model-facing `task_*` control tools | Product — stable API |
| [`jobs/`](jobs/README.md) | Generic background-job runtime and model-facing `job_*` control tools | Product — stable API |
| [`workflow/`](workflow/README.md) | Workflow seam, worker-thread engine, and model-facing `workflow`/`ralph` tools | Product — stable API |
| [`web/`](web/README.md) | Web capability family: seam, search/fetch provider impls, and the model-facing web tools | Product — stable API |
| [`attachment/`](attachment/README.md) | Durable attachment identity, validation, local content-addressed storage | Product — stable API |
@@ -39,7 +40,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 API |
| [`guard/`](guard/README.md) | Loop-hygiene guards: advisory repeat-call reminders + the `tools/execute` deadline enforcer | Product — stable API |
| [`bundle/`](bundle/README.md) | Installable `dsh --profile` patch layers | Product — stable API |
| [`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 API |
| [`extensions/`](extensions/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 API |
| [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable API |
| [`session/`](session/README.md) | Durable session data plane: persistence seam + JSONL/SQLite backends, projection seam, log-backed titles, session reporting | Product — stable API |
| [`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 API |
@@ -54,7 +55,7 @@ Groups hold `packages/<group>/<pkg>/`; names stay `@deepseek-ai/dsh-<pkg>`. **Gr
| [`host/`](host/README.md) | Web-GUI host half: API gateway + HTTP route server | Product — stable API |
| [`client/`](client/README.md) | Web-GUI browser half: shell, wire, object services, slots, `ui-*` plugins | Product — stable API |
| [`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 |
| [`test-support/`](test-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 |
New packages join existing groups; new groups update their README and this table.

View File

@@ -11,25 +11,26 @@ npm scope 为 `@deepseek-ai/dsh-*`Cordis `Service` 子类和函数插件通
| 组 | 职责 | 发布预期 |
|---|---|---|
| [`core/`](core/README.md) | 产品 API 主干会话、提示词、工具、agent智能体服务与具体循环 | 产品:稳定接口 |
| [`api/`](api/README.md) | Remote BFF 装配与 TypeRT RPC Gateway | 产品:稳定接口 |
| [`api/`](api/README.md) | Remote BFF 装配与 Typert RPC Gateway | 产品:稳定接口 |
| [`typert/`](typert/README.md) | 类型图生成、产物加载与运行时注册表 | 产品:稳定接口 |
| [`goal/`](goal/README.md) | 同会话 goal 的持久化与生命周期 | 产品:稳定接口 |
| [`schedule/`](schedule/README.md) | 仅限会话内的定时后续轮次 | 产品:稳定接口 |
| [`feedback/`](feedback/README.md) | 人类反馈 | 产品:稳定接口 |
| [`identity/`](identity/README.md) | 共享匿名身份 | 产品:稳定接口 |
| [`llm/`](llm/README.md) | LLM大语言模型能力系列抽象服务 + 提供方适配器 | 产品:稳定接口 |
| [`e2b/`](e2b/README.md) | E2B 提供方 | POC |
| [`subprocess/`](subprocess/README.md) | 子进程能力系列Service Definition + 本地进程树提供方 | 产品:稳定接口 |
| [`bash/`](bash/README.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 | 产品:稳定接口 |
| [`pty/`](pty/README.md) | 持久 PTY 能力系列:按所有者隔离的会话、本地实现和面向模型的工具 | 产品:稳定接口 |
| [`shell/`](shell/README.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 | 产品:稳定接口 |
| [`terminal/`](terminal/README.md) | 持久 PTY 能力系列:按所有者隔离的会话、本地实现和面向模型的工具 | 产品:稳定接口 |
| [`code-runtime/`](code-runtime/README.md) | 代码执行能力系列Service Definition + worker 线程提供方 + Code Mode Consumer | 产品:稳定接口 |
| [`sandbox/`](sandbox/README.md) | 进程限制 seambwrap/Landlock/Seatbelt 后端 | 产品:稳定接口 |
| [`fs/`](fs/README.md) | 文件系统能力系列seam、本地实现、面向模型的文件工具、由 bash 支持的发现工具 | 产品:稳定接口 |
| [`lsp/`](lsp/README.md) | LSP 能力系列seam、通用 stdio 提供方和 `lsp` 工具 | 产品:稳定接口 |
| [`skill/`](skill/README.md) | skill技能能力系列提供方注册表、本地提供方和面向模型的目录loader | 产品:稳定接口 |
| [`compact/`](compact/README.md) | 压缩compaction能力系列Service Definition + 基础提供方 + 命令 Consumer | 产品:稳定接口 |
| [`compaction/`](compaction/README.md) | 压缩compaction能力系列Service Definition + 基础提供方 + 命令 Consumer | 产品:稳定接口 |
| [`context/`](context/README.md) | 模型可见请求上下文,包括 workspace 指令和时间上下文 | 产品:稳定接口 |
| [`subagent/`](subagent/README.md) | subagent 能力系列:提供方注册表约定和面向模型的委托工具 | 产品:稳定接口 |
| [`tasks/`](tasks/README.md) | 通用后台任务运行时和面向模型的 `task_*` 控制工具 | 产品:稳定接口 |
| [`jobs/`](jobs/README.md) | 通用后台任务运行时和面向模型的 `job_*` 控制工具 | 产品:稳定接口 |
| [`workflow/`](workflow/README.md) | 工作流 seam、worker 线程引擎和面向模型的 `workflow`/`ralph` 工具 | 产品:稳定接口 |
| [`web/`](web/README.md) | Web 能力系列seam、搜索获取提供方实现和面向模型的 Web 工具 | 产品:稳定接口 |
| [`attachment/`](attachment/README.md) | 持久附件标识、校验、本地内容寻址存储 | 产品:稳定接口 |
@@ -39,7 +40,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) | 产品:稳定接口 |
| [`extensions/`](extensions/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 全文搜索 | 产品:稳定接口 |
@@ -54,7 +55,7 @@ npm scope 为 `@deepseek-ai/dsh-*`Cordis `Service` 子类和函数插件通
| [`host/`](host/README.md) | web GUI 宿主半侧API 网关 + HTTP 路由服务器 | 产品:稳定接口 |
| [`client/`](client/README.md) | web GUI 浏览器半侧shell、协议层、对象服务、slot、`ui-*` 插件 | 产品:稳定接口 |
| [`examples/`](examples/README.md) | 演示组合包agent-spine + CLI命令行界面/ACP/JSON-RPC bin由叶节点加载 | 支持:示例基础设施 |
| [`support/`](support/README.md) | 支持基础设施testkit、不变式、回放、Loader 冒烟测试) | 支持:兼容性预期较低 |
| [`test-support/`](test-support/README.md) | 支持基础设施testkit、不变式、回放、Loader 冒烟测试) | 支持:兼容性预期较低 |
| [`util/`](util/README.md) | 组间共享的低层零依赖工具(`Branded<B>`、Harness home路径辅助函数、超时、保留策略 | 支持:小型、稳定、无 harness 依赖 |
新包加入现有组;新组更新其 README 和此表。

View File

@@ -1,7 +1,7 @@
{
"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-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},

View File

@@ -27,7 +27,7 @@
"path": "../../interaction/user-approval"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

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/api/README.md
README.md: 7c75e8012459266e0ce09c97416d140e5ac777e1
README.zh.md: f4e882570d2dc929cca466e0bdb05bb2943f2c93
README.md: 2db5c518f75a1bba5790146a5f1b91a4fa5bf745
README.zh.md: 1fc41ee43c184054a558e6fa8ebe7de0e87530b6

View File

@@ -2,14 +2,14 @@
English | [中文](README.zh.md)
The application-facing Remote stack. `remotes` owns BFF policy and the selected business API, while `gateway` implements the TypeRT unary RPC endpoints shared by Host and Client environments.
The application-facing Remote stack. `remotes` owns BFF policy and the selected business API, while `gateway` implements the Typert unary RPC endpoints shared by Host and Client environments.
| Package | Role | ctx key |
|---|---|---|
| [`remotes/`](remotes/README.md) | Host Agent/Session lookup policy and Client Remote contribution assembly | no service; configures `ctx.typert` and consumes `ctx.remote` |
| [`gateway/`](gateway/README.md) | Host TypeRT dispatcher and Client Remote endpoint | `ctx.typertGateway` / `ctx.remote` |
| [`gateway/`](gateway/README.md) | Host Typert dispatcher and Client Remote endpoint | `ctx.typertGateway` / `ctx.remote` |
The runtime dependency direction is `remotes → gateway → connection → webserver`: the BFF consumes the shared `TypeRTClientRemote` contract, Gateway delegates transport to Connection, and Connection mounts on the HTTP server. Cordis service injection and Client module metadata preserve this order without importing the concrete Gateway from the Remotes Client entry.
The runtime dependency direction is `remotes → gateway → connection → webserver`: the BFF consumes the shared `TypertClientRemote` contract, Gateway delegates transport to Connection, and Connection mounts on the HTTP server. Cordis service injection and Client module metadata preserve this order without importing the concrete Gateway from the Remotes Client entry.
## Known Limitations and Deferred Work

View File

@@ -2,14 +2,14 @@
[English](README.md) | 中文
面向应用的 Remote 技术栈。`remotes` 负责 BFF 策略和选定的业务 API`gateway` 则实现 Host 与 Client 环境共用的 TypeRT 一元 RPC endpoint。
面向应用的 Remote 技术栈。`remotes` 负责 BFF 策略和选定的业务 API`gateway` 则实现 Host 与 Client 环境共用的 Typert 一元 RPC endpoint。
| 包 | 职责 | ctx key |
|---|---|---|
| [`remotes/`](remotes/README.md) | Host Agent/Session lookup 策略与 Client Remote contribution 装配 | 无服务;配置 `ctx.typert` 并消费 `ctx.remote` |
| [`gateway/`](gateway/README.md) | Host TypeRT 分发器与 Client Remote endpoint | `ctx.typertGateway` / `ctx.remote` |
| [`gateway/`](gateway/README.md) | Host Typert 分发器与 Client Remote endpoint | `ctx.typertGateway` / `ctx.remote` |
运行时依赖方向为 `remotes → gateway → connection → webserver`BFF 消费共享的 `TypeRTClientRemote` 约定Gateway 把传输交给 ConnectionConnection 再挂载到 HTTP server。Cordis 服务注入与 Client 模块元数据在不让 Remotes Client 入口导入具体 Gateway 实现的前提下维持该顺序。
运行时依赖方向为 `remotes → gateway → connection → webserver`BFF 消费共享的 `TypertClientRemote` 约定Gateway 把传输交给 ConnectionConnection 再挂载到 HTTP server。Cordis 服务注入与 Client 模块元数据在不让 Remotes Client 入口导入具体 Gateway 实现的前提下维持该顺序。
## 已知限制与延期工作

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/api/gateway/README.md
README.md: 96f55eead6aec50e2f39f5bcefe71b51cc853298
README.zh.md: 8985fd1f57833bc05f045e72135bb69a5a198d50
README.md: 7caf707c376bd3e2654fad1f0c01ac83e6faa44c
README.zh.md: b340df982ed1ca9b980fbcf60729d8124319ce6e

View File

@@ -2,15 +2,15 @@
English | [中文](README.zh.md)
Two-sided TypeRT RPC endpoint for Host and Client Cordis environments. The Host entry provides `ctx.typertGateway`, while `@deepseek-ai/dsh-api-gateway/client` provides `ctx.remote`; both consume the same generated `InvocationDescriptor` contract and leave business selection to API Remotes and transport, request correlation, trust, and response envelopes to Connection.
Two-sided Typert RPC endpoint for Host and Client Cordis environments. The Host entry provides `ctx.typertGateway`, while `@deepseek-ai/dsh-api-gateway/client` provides `ctx.remote`; both consume the same generated `InvocationDescriptor` contract and leave business selection to API Remotes and transport, request correlation, trust, and response envelopes to Connection.
## Host service: `TypertGatewayService` (ctx key: `typertGateway`)
`ctx.typertGateway.invoke()` resolves the current descriptor and Cordis Service for each call, validates exact named arguments, resolves registered object or Context identities, invokes the public business method, and validates its result. Business Services extend `GatewayService` and mark methods with `@Remote` or `@RemoteScope` from [`dsh-type-meta`](../../typert/type-meta/README.md); `bindTypeRTGateway()` remains available when another base class owns inheritance.
`ctx.typertGateway.invoke()` resolves the current descriptor and Cordis Service for each call, validates exact named arguments, resolves registered object or Context identities, invokes the public business method, and validates its result. Business Services extend `TypertRemoteService` and mark methods with `@Remote` or `@RemoteScope` from [`dsh-typert-protocol`](../../typert/protocol/README.md); `bindTypertRemote()` remains available when another base class owns inheritance.
Strict mode reads generated invocation descriptors from `ctx.typert.local`. Lookup parameters use the currently active resolver in `ctx.typert.lookups`: the business package registers the stable declaration and default policy, while Host composition can override resolution behavior with effect-scoped `configure()`; `@RemoteScope` resolves its receiver through a registered Host Context provider. SRC mode is a development fallback for endpoints that have never had a strict definition; it parses simple parameter names and accepts only JSON-safe values for non-lookup parameters. Withdrawing an observed strict definition fails instead of weakening validation.
The Host entry registers a trusted-host interceptor on Connection's shared `/api` FetchHandler. Connection passes this composite handler through its HTTP bridge; the handler dispatches claimed endpoints to Gateway and unclaimed endpoints to API Proxy. Direct `invoke()` calls preserve business errors; `TypertGatewayError` distinguishes failures owned by dispatch, binding, providers, lookup, Context, arguments, and codecs. A resolver may use `TypeRTLookupFailure` to carry an existing RPC error, preserving its original error code for policy rejections such as cold-resume failures or ownership fences.
The Host entry registers a trusted-host interceptor on Connection's shared `/api` FetchHandler. Connection passes this composite handler through its HTTP bridge; the handler dispatches claimed endpoints to Gateway and unclaimed endpoints to API Proxy. Direct `invoke()` calls preserve business errors; `TypertGatewayError` distinguishes failures owned by dispatch, binding, providers, lookup, Context, arguments, and codecs. A resolver may use `TypertLookupFailure` to carry an existing RPC error, preserving its original error code for policy rejections such as cold-resume failures or ownership fences.
A cancellation-aware Remote method declares `signal: AbortSignal` as its final Host parameter. The signal is descriptor metadata rather than a wire argument: Connection supplies it to the Gateway, and the Gateway injects it after decoded business parameters. SRC recognizes the reserved final name, while strict generation additionally requires the global `AbortSignal` type.
@@ -22,7 +22,7 @@ Each call validates positional inputs, constructs the descriptor's exact named `
`ctx.remote.$on()` subscribes to one forwarded Host event. Its legal keys are exactly the Host assembly's forwarding selection, and the listener type is the owning package's own Cordis `Events` declaration, so no second signature can drift from it. Each subscription belongs to the calling fiber and disappears with it. Delivery is one-way and follows registration order; a listener that throws is logged and isolated from the remaining listeners, which never affects the frame pump. `ctx.remote.$dispatch()` is the other half of that surface, and it is the carrier's: the Client half owning the Host frame sink hands each decoded frame over, and an event name nobody subscribes to is dropped, since the wire carries whatever the Host selected. A consumer subscribes and never calls it.
Generated declaration merges provide the TypeScript API through the shared `TypeRTClientRemote` contract. The Client entry contains no Host Service or Host Cordis interface merge, and method lookup and invocation use ordinary objects and functions rather than a JavaScript Proxy.
Generated declaration merges provide the TypeScript API through the shared `TypertClientRemote` contract. The Client entry contains no Host Service or Host Cordis interface merge, and method lookup and invocation use ordinary objects and functions rather than a JavaScript Proxy.
## Model Experience
@@ -34,7 +34,7 @@ No direct effect; invoked business Services own any model-visible result.
## Known Limitations and Deferred Work
- The Connection adapter maps ordinary dispatch failures and business exceptions to the RPC `internal` code with empty details; lookup-policy errors carried by `TypeRTLookupFailure` are returned unchanged. Structured `TypertGatewayError` categories remain available only to same-process callers.
- The Connection adapter maps ordinary dispatch failures and business exceptions to the RPC `internal` code with empty details; lookup-policy errors carried by `TypertLookupFailure` are returned unchanged. Structured `TypertGatewayError` categories remain available only to same-process callers.
- SRC mode supports unique identifier parameters without destructuring, defaults, or rest parameters. It validates JSON safety rather than generated business types and never infers optional fields.
- Only strict generated contributions can mount on the Client face. SRC markers have no Client codec or type projection.
- The package dispatches unary methods only. Incremental Session data uses a separate named-stream protocol over the same Connection.

View File

@@ -2,15 +2,15 @@
[English](README.md) | 中文
为 Host 与 Client 两侧的 Cordis 环境提供 TypeRT RPC endpoint。Host 入口提供 `ctx.typertGateway``@deepseek-ai/dsh-api-gateway/client` 则提供 `ctx.remote`;两者使用同一份生成的 `InvocationDescriptor` 约定,并将业务选择交给 API Remotes将传输、请求关联、信任和响应封装交给 Connection。
为 Host 与 Client 两侧的 Cordis 环境提供 Typert RPC endpoint。Host 入口提供 `ctx.typertGateway``@deepseek-ai/dsh-api-gateway/client` 则提供 `ctx.remote`;两者使用同一份生成的 `InvocationDescriptor` 约定,并将业务选择交给 API Remotes将传输、请求关联、信任和响应封装交给 Connection。
## Host 服务:`TypertGatewayService`ctx key`typertGateway`
每次调用时,`ctx.typertGateway.invoke()` 都会解析当前的描述符和 Cordis 服务,校验具名参数是否完全匹配,解析已注册的对象或 Context 身份标识,调用公开的业务方法,并校验其结果。业务服务继承 [`dsh-type-meta`](../../typert/type-meta/README.md) 的 `GatewayService`,并用 `@Remote``@RemoteScope` 标记方法;已有其他基类时仍可改用 `bindTypeRTGateway()`
每次调用时,`ctx.typertGateway.invoke()` 都会解析当前的描述符和 Cordis 服务,校验具名参数是否完全匹配,解析已注册的对象或 Context 身份标识,调用公开的业务方法,并校验其结果。业务服务继承 [`dsh-typert-protocol`](../../typert/protocol/README.md) 的 `TypertRemoteService`,并用 `@Remote``@RemoteScope` 标记方法;已有其他基类时仍可改用 `bindTypertRemote()`
严格模式从 `ctx.typert.local` 读取生成的调用描述符。查找参数使用 `ctx.typert.lookups` 中当前有效的 resolver业务包注册稳定声明与默认策略Host 组合可用 effect-scoped `configure()` 覆盖解析行为;`@RemoteScope` 则通过已注册的 Host Context 提供方解析其接收者。SRC 模式是开发阶段的回退路径,适用于从未具备严格定义的端点;它解析简单参数名,并且只允许非查找参数使用可安全表示为 JSON 的值。已观测到的严格定义一旦撤回,系统会直接报错,而不会降低校验强度。
Connection 可用时Host 入口会在 Connection 共享的 `/api` FetchHandler 上注册 trusted-host interceptor。Connection 把这个复合 handler 交给 HTTP bridgehandler 将已认领 endpoint 分发给 Gateway未认领 endpoint 则交给 API Proxy。直接调用 `invoke()` 会保留业务错误;`TypertGatewayError` 可区分分发、绑定、提供方、查找、Context、参数和编解码器各自负责的故障。resolver 可以用 `TypeRTLookupFailure` 携带既有 RPC error使冷恢复失败或 ownership fence 等策略拒绝保持原错误码。
Connection 可用时Host 入口会在 Connection 共享的 `/api` FetchHandler 上注册 trusted-host interceptor。Connection 把这个复合 handler 交给 HTTP bridgehandler 将已认领 endpoint 分发给 Gateway未认领 endpoint 则交给 API Proxy。直接调用 `invoke()` 会保留业务错误;`TypertGatewayError` 可区分分发、绑定、提供方、查找、Context、参数和编解码器各自负责的故障。resolver 可以用 `TypertLookupFailure` 携带既有 RPC error使冷恢复失败或 ownership fence 等策略拒绝保持原错误码。
支持取消的 Remote 方法会把 `signal: AbortSignal` 声明为最后一个 Host 参数。signal 是 descriptor 元数据,而不是 wire 参数Connection 将它提供给 GatewayGateway 则在已解码的业务参数之后注入它。SRC 识别这个保留的末位参数名,严格生成还要求它具有全局 `AbortSignal` 类型。
@@ -22,7 +22,7 @@ Connection 可用时Host 入口会在 Connection 共享的 `/api` FetchHandle
`ctx.remote.$on()` 订阅一条被转发的 Host 事件。它的合法键恰好等于 Host 装配声明的转发选择listener 类型就是事件所属包自己的 Cordis `Events` 声明,因此不存在会与之漂移的第二份签名。每个订阅归属发起调用的 fiber并随该 fiber 一起消失。投递是单向的,并按注册顺序进行;抛错的 listener 会被记录并与其余 listener 隔离,绝不影响帧泵。`ctx.remote.$dispatch()` 是该面的另一半,且属于载体:持有 Host 帧 sink 的 Client 半把每个解码后的帧交进来,收到无人订阅的事件名即丢弃,因为 wire 上出现什么取决于 Host 的转发选择。消费方只订阅,绝不调用它。
生成的声明合并通过共享的 `TypeRTClientRemote` 约定提供 TypeScript API。Client 入口不包含 Host 服务或 Host Cordis 接口合并;方法查找和调用使用普通对象与函数,而不使用 JavaScript Proxy。
生成的声明合并通过共享的 `TypertClientRemote` 约定提供 TypeScript API。Client 入口不包含 Host 服务或 Host Cordis 接口合并;方法查找和调用使用普通对象与函数,而不使用 JavaScript Proxy。
## 模型体验
@@ -34,7 +34,7 @@ Connection 可用时Host 入口会在 Connection 共享的 `/api` FetchHandle
## 已知限制与延期工作
- Connection 适配器将普通分发故障和业务异常映射为 RPC 的 `internal` 代码,且不附带详细信息;`TypeRTLookupFailure` 携带的 lookup 策略错误会原样返回。结构化的 `TypertGatewayError` 类别仅供同进程调用方使用。
- Connection 适配器将普通分发故障和业务异常映射为 RPC 的 `internal` 代码,且不附带详细信息;`TypertLookupFailure` 携带的 lookup 策略错误会原样返回。结构化的 `TypertGatewayError` 类别仅供同进程调用方使用。
- SRC 模式仅支持名称唯一的标识符参数,不支持解构、默认值或剩余参数。它只校验值能否安全表示为 JSON不校验生成的业务类型也绝不会推断可选字段。
- Client 侧只能挂载严格模式生成的贡献项。SRC 标记不具备 Client 编解码器或类型投影。
- 该包只分发一元方法。增量会话数据通过同一个 Connection 上独立的具名流协议传输。

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-gateway",
"description": "TypeRT Remote Host dispatcher and Client API endpoint",
"version": "0.0.1-rc.2",
"description": "Typert Remote Host dispatcher and Client API endpoint",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},
@@ -56,7 +56,7 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"@deepseek-ai/dsh-type-meta": "workspace:^"
"@deepseek-ai/dsh-typert-protocol": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/dsh-client-connection": "workspace:^",

View File

@@ -1,5 +1,5 @@
/**
* Client projection of generated TypeRT Remote descriptors. Contributions
* Client projection of generated Typert Remote descriptors. Contributions
* install traced `remote.<namespace>` services; no JavaScript Proxy
* participates in method lookup, invocation, or type exposure.
*/
@@ -9,13 +9,13 @@ import type { Context, Events } from '@deepseek-ai/cordis'
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
import type {
InvocationDescriptor,
TypeRTClientRemote,
TypertClientRemote,
RemoteResult,
TypeRTCodec,
TypeRTDisposer,
TypeRTRemoteContribution,
TypeRTRemoteEvent,
} from '@deepseek-ai/dsh-type-meta'
TypertCodec,
TypertDisposer,
TypertRemoteContribution,
TypertRemoteEvent,
} from '@deepseek-ai/dsh-typert-protocol'
interface MountToken {
active: boolean
@@ -25,7 +25,7 @@ interface MountToken {
interface ScopedProjection {
readonly context: string
readonly wire: string
readonly codec: TypeRTCodec
readonly codec: TypertCodec
readonly parameterIndex?: number
}
@@ -49,11 +49,11 @@ interface BoundContextIdentity {
interface RemoteNamespaceHandle {
readonly service: RemoteNamespaceService
readonly dispose: TypeRTDisposer
readonly dispose: TypertDisposer
}
/** Typed Remote service augmented by generated direct namespaces. */
export type ClientRemote = TypeRTClientRemote
export type ClientRemote = TypertClientRemote
declare module '@deepseek-ai/cordis' {
interface Context {
@@ -62,7 +62,7 @@ declare module '@deepseek-ai/cordis' {
}
}
/** Required Client services: the TypeRT registry and the existing Connection carrier. */
/** Required Client services: the Typert registry and the existing Connection carrier. */
export const inject = ['typert', 'connection']
/**
@@ -85,7 +85,7 @@ interface RemoteEventSubscription {
readonly listener: RemoteEventListener
}
class ClientRemoteService extends Service implements TypeRTClientRemote {
class ClientRemoteService extends Service implements TypertClientRemote {
private readonly ownerCtx: Context
private readonly namespaces = new Map<string, RemoteNamespaceHandle>()
private readonly subscriptions = new Map<string, RemoteEventSubscription[]>()
@@ -97,7 +97,7 @@ class ClientRemoteService extends Service implements TypeRTClientRemote {
ctx.effect(() => () => { this.subscriptions.clear() }, 'api-gateway.client.subscriptions')
}
async $mount(contribution: TypeRTRemoteContribution): ReturnType<TypeRTClientRemote['$mount']> {
async $mount(contribution: TypertRemoteContribution): ReturnType<TypertClientRemote['$mount']> {
const callerCtx = this.ctx
const owned = callerCtx.effect(async () => {
const dispose = await this.enqueue(() => this.mountContribution(callerCtx, contribution))
@@ -107,10 +107,10 @@ class ClientRemoteService extends Service implements TypeRTClientRemote {
return async () => { await owned() }
}
$on<Event extends TypeRTRemoteEvent>(
$on<Event extends TypertRemoteEvent>(
event: Event,
listener: Events[Event],
): ReturnType<TypeRTClientRemote['$on']> {
): ReturnType<TypertClientRemote['$on']> {
// The table is keyed by the runtime event name, so the argument list this
// signature pins per event cannot survive in it; `$deliver` restores it
// from the frame the Host emitted for that same name.
@@ -130,7 +130,7 @@ class ClientRemoteService extends Service implements TypeRTClientRemote {
/**
* Deliver one forwarded event in registration order, isolating a listener
* that fails either synchronously or by rejecting a returned promise; see
* {@link TypeRTClientRemote.$dispatch} for the caller contract.
* {@link TypertClientRemote.$dispatch} for the caller contract.
*/
$dispatch(event: string, args: readonly unknown[]): void {
const listeners = this.subscriptions.get(event)
@@ -173,11 +173,11 @@ class ClientRemoteService extends Service implements TypeRTClientRemote {
private async mountContribution(
callerCtx: Context,
contribution: TypeRTRemoteContribution,
): Promise<TypeRTDisposer> {
contribution: TypertRemoteContribution,
): Promise<TypertDisposer> {
this.validateContribution(contribution)
const disposeRemote = callerCtx.typert.remotes.register(contribution)
const installed: TypeRTDisposer[] = []
const installed: TypertDisposer[] = []
try {
for (const descriptor of contribution.descriptors) installed.push(await this.install(descriptor))
} catch (error) {
@@ -191,7 +191,7 @@ class ClientRemoteService extends Service implements TypeRTClientRemote {
}
}
private validateContribution(contribution: TypeRTRemoteContribution): void {
private validateContribution(contribution: TypertRemoteContribution): void {
const direct = new Map<string, Set<string>>()
const scoped = new Map<string, Set<string>>()
const add = (
@@ -235,9 +235,9 @@ class ClientRemoteService extends Service implements TypeRTClientRemote {
}
}
private async install(descriptor: InvocationDescriptor): Promise<TypeRTDisposer> {
private async install(descriptor: InvocationDescriptor): Promise<TypertDisposer> {
const token: MountToken = { active: true, abort: new AbortController() }
const installed: TypeRTDisposer[] = []
const installed: TypertDisposer[] = []
try {
if (descriptor.invocation.kind === 'direct') {
installed.push(await this.installDirect(descriptor, token))
@@ -259,7 +259,7 @@ class ClientRemoteService extends Service implements TypeRTClientRemote {
}
}
private async installDirect(descriptor: InvocationDescriptor, token: MountToken): Promise<TypeRTDisposer> {
private async installDirect(descriptor: InvocationDescriptor, token: MountToken): Promise<TypertDisposer> {
const namespace = await this.namespace(descriptor.namespace)
try {
namespace.service.installDirect(descriptor, token)
@@ -277,7 +277,7 @@ class ClientRemoteService extends Service implements TypeRTClientRemote {
descriptor: InvocationDescriptor,
projection: ScopedProjection,
token: MountToken,
): Promise<TypeRTDisposer> {
): Promise<TypertDisposer> {
const namespace = await this.namespace(descriptor.namespace)
try {
namespace.service.installScoped(descriptor, projection, token)
@@ -557,13 +557,13 @@ function requireStrictDescriptor(descriptor: InvocationDescriptor): void {
}
}
function requireStrictCodec(codec: TypeRTCodec, endpoint: string, field: string): void {
function requireStrictCodec(codec: TypertCodec, endpoint: string, field: string): void {
if (codec.mode !== 'strict') {
throw new Error(`client api: generated Remote ${endpoint} field ${JSON.stringify(field)} has no strict codec`)
}
}
function parse(codec: TypeRTCodec, value: unknown, endpoint: string, field: string): unknown {
function parse(codec: TypertCodec, value: unknown, endpoint: string, field: string): unknown {
if (codec.mode !== 'strict') {
throw new Error(`client api: generated Remote ${endpoint} field ${JSON.stringify(field)} has no strict codec`)
}

View File

@@ -1,5 +1,5 @@
/**
* Live TypeRT Remote dispatch over Cordis Services and registered providers.
* Live Typert Remote dispatch over Cordis Services and registered providers.
* Transport, request correlation, and response envelopes belong to Connection.
* @module @deepseek-ai/dsh-api-gateway
*/
@@ -8,12 +8,12 @@ import { Context, Service, symbols } from '@deepseek-ai/cordis'
import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection'
import {
remoteMethods,
TypeRTLookupFailure,
TypertLookupFailure,
type InvocationDescriptor,
type InvocationParameterDescriptor,
type TypeRTCodec,
type TypeRTGatewayBinding,
} from '@deepseek-ai/dsh-type-meta'
type TypertCodec,
type TypertGatewayBinding,
} from '@deepseek-ai/dsh-typert-protocol'
import type {
InvokeRemoteRequest,
TypertGateway,
@@ -32,7 +32,7 @@ interface GatewayErrorOptions {
}
interface ResolvedBinding {
readonly binding: TypeRTGatewayBinding
readonly binding: TypertGatewayBinding
readonly original: object
}
@@ -84,7 +84,7 @@ class RemoteInvocationCancelled extends Error {
/**
* Resolve strict generated definitions or conservative SRC markers against
* current Cordis Services and TypeRT providers.
* current Cordis Services and Typert providers.
* @typert service typertGateway
*/
export class TypertGatewayService extends Service implements TypertGateway {
@@ -93,8 +93,8 @@ export class TypertGatewayService extends Service implements TypertGateway {
private srcClaims: ReadonlySet<string> | undefined
/**
* Register the Gateway against the active TypeRT registry.
* @param ctx - owning Host Context with TypeRT registry access.
* Register the Gateway against the active Typert registry.
* @param ctx - owning Host Context with Typert registry access.
*/
constructor(ctx: Context) {
super(ctx, 'typertGateway')
@@ -126,7 +126,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
const receiver = this.ctx.get(serviceKey) as unknown
if (!isObject(receiver)) continue
const original = originalOf(receiver)
const binding = Reflect.get(original, 'typertGateway') as unknown
const binding = Reflect.get(original, 'typertRemote') as unknown
if (!isObject(binding) || typeof Reflect.get(binding, 'namespace') !== 'string') continue
const namespace = Reflect.get(binding, 'namespace') as string
for (const candidate of remoteMethods(original)) {
@@ -241,7 +241,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
const receiver = this.ctx.get(serviceKey) as unknown
if (!isObject(receiver)) continue
const original = originalOf(receiver)
const value = Reflect.get(original, 'typertGateway') as unknown
const value = Reflect.get(original, 'typertRemote') as unknown
if (value === undefined) continue
const binding = readBinding(value, original, serviceKey, endpoint)
if (binding.namespace !== namespace) continue
@@ -263,7 +263,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
}
private srcDescriptor(
binding: TypeRTGatewayBinding,
binding: TypertGatewayBinding,
marker: ReturnType<typeof remoteMethods>[number],
method: string,
endpoint: string,
@@ -385,7 +385,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
try {
context = await provider.resolve(identity)
} catch (cause) {
if (cause instanceof TypeRTLookupFailure) throw cause
if (cause instanceof TypertLookupFailure) throw cause
throw new TypertGatewayError(
'context-failed',
endpoint,
@@ -448,7 +448,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
try {
resolved = await provider.resolve(value)
} catch (cause) {
if (cause instanceof TypeRTLookupFailure) throw cause
if (cause instanceof TypertLookupFailure) throw cause
throw new TypertGatewayError(
'lookup-failed',
endpoint,
@@ -475,7 +475,7 @@ function rpcFailure(error: unknown): ConnectionRpcResult {
error: { code: 'cancelled', message: error.message, details: {} },
}
}
if (error instanceof TypeRTLookupFailure) {
if (error instanceof TypertLookupFailure) {
return { ok: false, error: error.failure as ConnectionRpcError }
}
return {
@@ -499,12 +499,12 @@ function validateBinding(
endpoint: string,
): ResolvedBinding {
const original = originalOf(receiver)
const value = Reflect.get(original, 'typertGateway') as unknown
const value = Reflect.get(original, 'typertRemote') as unknown
if (value === undefined) {
throw new TypertGatewayError(
'binding-invalid',
endpoint,
`Service ${JSON.stringify(serviceKey)} has no visible typertGateway binding`,
`Service ${JSON.stringify(serviceKey)} has no visible typertRemote binding`,
)
}
return {
@@ -519,7 +519,7 @@ function readBinding(
serviceKey: string,
endpoint: string,
namespace?: string,
): TypeRTGatewayBinding {
): TypertGatewayBinding {
if (!isObject(value)
|| Reflect.get(value, 'service') !== original
|| Reflect.get(value, 'serviceKey') !== serviceKey
@@ -528,10 +528,10 @@ function readBinding(
throw new TypertGatewayError(
'binding-invalid',
endpoint,
`Service ${JSON.stringify(serviceKey)} has an inconsistent typertGateway binding`,
`Service ${JSON.stringify(serviceKey)} has an inconsistent typertRemote binding`,
)
}
return value as unknown as TypeRTGatewayBinding
return value as unknown as TypertGatewayBinding
}
function originalOf(receiver: object): object {
@@ -612,7 +612,7 @@ function assertExactArguments(
}
function decode(
codec: TypeRTCodec,
codec: TypertCodec,
value: unknown,
code: 'input-invalid' | 'result-invalid',
endpoint: string,

View File

@@ -15,7 +15,7 @@ export const name = 'api-gateway-invariant'
export const inject = ['invariants']
/**
* No runtime invariant: Host calls re-read authoritative Cordis and TypeRT
* No runtime invariant: Host calls re-read authoritative Cordis and Typert
* state, while Client methods, descriptors, and `$on` subscriptions mutate in
* one owned effect.
*/

View File

@@ -1,5 +1,5 @@
/**
* Carrier-independent TypeRT Gateway request, service, and error contracts.
* Carrier-independent Typert Gateway request, service, and error contracts.
* @module @deepseek-ai/dsh-api-gateway/types
*/
@@ -48,7 +48,7 @@ export interface TypertGateway {
declare module '@deepseek-ai/cordis' {
interface Context {
/** Host dispatcher for TypeRT Remote calls. */
/** Host dispatcher for Typert Remote calls. */
typertGateway: TypertGateway
}
}

View File

@@ -6,11 +6,11 @@ import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client
import type {
InvocationDescriptor,
RemoteResult,
TypeRTClientRemote,
TypeRTContext,
TypeRTRemoteScopeApi,
TypeRTRemoteNamespace,
} from '@deepseek-ai/dsh-type-meta'
TypertClientRemote,
TypertContext,
TypertRemoteScopeApi,
TypertRemoteNamespace,
} from '@deepseek-ai/dsh-typert-protocol'
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
import type { ClientRemote } from '../src/client/index.ts'
import { apply, inject } from '../src/client/index.ts'
@@ -35,14 +35,14 @@ declare module '@deepseek-ai/cordis' {
}
}
declare module '@deepseek-ai/dsh-type-meta' {
interface TypeRTRemoteEventSelection extends Record<'fixture/changed' | 'fixture/idle', true> {}
declare module '@deepseek-ai/dsh-typert-protocol' {
interface TypertRemoteEventSelection extends Record<'fixture/changed' | 'fixture/idle', true> {}
interface TypeRTContextMap {
fixture: TypeRTContext<string>
interface TypertContextMap {
fixture: TypertContext<string>
}
interface TypeRTRemoteMap {
interface TypertRemoteMap {
'probe/create': (
agentId: string,
request: { readonly objective: string },
@@ -51,7 +51,7 @@ declare module '@deepseek-ai/dsh-type-meta' {
'probe/maybe': (value: string | null | undefined) => Promise<RemoteResult<string | null | undefined>>
}
interface TypeRTRemoteScopeMap {
interface TypertRemoteScopeMap {
'fixture:probe/create': (
request: { readonly objective: string },
signal?: AbortSignal,
@@ -61,14 +61,14 @@ declare module '@deepseek-ai/dsh-type-meta' {
) => Promise<RemoteResult<{ readonly renamed: boolean }>>
}
interface TypeRTRemoteNamespaceMap {
probe: TypeRTRemoteNamespace<'probe'>
interface TypertRemoteNamespaceMap {
probe: TypertRemoteNamespace<'probe'>
}
}
type FixtureContext = Omit<Context, 'remote'> & {
readonly remote: TypeRTClientRemote & TypeRTRemoteScopeApi<'fixture'>
readonly remote: TypertClientRemote & TypertRemoteScopeApi<'fixture'>
}
// Compile-time contract of `$on`: the key face is the forwarding selection and
@@ -171,7 +171,7 @@ async function benchFiber(
return { ctx, client }
}
describe('Client TypeRT API', () => {
describe('Client Typert API', () => {
it('mounts concrete direct methods, validates both boundaries, and withdraws retained handles', async () => {
const call = vi.fn<ConnectionHandle['rpc']['call']>()
.mockResolvedValue({ ok: true, value: { ref: 'goal-1' } })

View File

@@ -4,17 +4,17 @@ import { describe, expect, it } from 'vitest'
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'
import type { WebServer, WebRoute } from '@deepseek-ai/dsh-host-webserver'
import {
bindTypeRTGateway,
bindTypertRemote,
Remote,
RemoteScope,
TypeRTLookupFailure,
TypertLookupFailure,
type InvocationDescriptor,
type TypeRTContext,
type TypeRTLookup,
type TypeRTLookupProvider,
} from '@deepseek-ai/dsh-type-meta'
type TypertContext,
type TypertLookup,
type TypertLookupProvider,
} from '@deepseek-ai/dsh-typert-protocol'
import TypertRegistry, { type TypertContribution } from '@deepseek-ai/dsh-typert-registry'
import TypertGatewayService, { TypertGatewayError } from '@deepseek-ai/dsh-api-gateway'
@@ -26,14 +26,14 @@ interface MarkedContext extends Context {
readonly fixtureScope?: string
}
declare module '@deepseek-ai/dsh-type-meta' {
interface TypeRTLookupMap {
gatewayFixture: TypeRTLookup<FixtureAgent, string>
gatewayFixtureAlias: TypeRTLookup<FixtureAgent, string>
declare module '@deepseek-ai/dsh-typert-protocol' {
interface TypertLookupMap {
gatewayFixture: TypertLookup<FixtureAgent, string>
gatewayFixtureAlias: TypertLookup<FixtureAgent, string>
}
interface TypeRTContextMap {
gatewayFixture: TypeRTContext<string>
interface TypertContextMap {
gatewayFixture: TypertContext<string>
}
}
@@ -44,7 +44,7 @@ const emptyModel: TypertContribution['model'] = {
}
class GoalService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'goals')
readonly typertRemote = bindTypertRemote(this, 'goals')
readonly calls: string[] = []
lastSignal: AbortSignal | undefined
nextResult: unknown = undefined
@@ -137,7 +137,7 @@ class FakeConnectionService extends Service {
}
}
function fakeHttpServer(routes: WebRoute[]): Pick<HttpServerService, 'register' | 'tapIndex' | 'port'> {
function fakeHttpServer(routes: WebRoute[]): Pick<WebServer, 'register' | 'tapIndex' | 'port'> {
return {
register(route) {
if (routes.some(candidate => candidate.kind === route.kind && candidate.path === route.path)) {
@@ -169,7 +169,7 @@ async function serveRoute(route: WebRoute): Promise<{ readonly origin: string; c
}
class FirstSharedService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'firstShared', { namespace: 'shared' })
readonly typertRemote = bindTypertRemote(this, 'firstShared', { namespace: 'shared' })
constructor(ctx: Context) {
super(ctx, 'firstShared')
@@ -182,7 +182,7 @@ class FirstSharedService extends Service {
}
class SecondSharedService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'secondShared', { namespace: 'shared' })
readonly typertRemote = bindTypertRemote(this, 'secondShared', { namespace: 'shared' })
constructor(ctx: Context) {
super(ctx, 'secondShared')
@@ -195,7 +195,7 @@ class SecondSharedService extends Service {
}
class DefaultParameterService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'defaultParameter', { namespace: 'invalid-default' })
readonly typertRemote = bindTypertRemote(this, 'defaultParameter', { namespace: 'invalid-default' })
constructor(ctx: Context) {
super(ctx, 'defaultParameter')
@@ -208,7 +208,7 @@ class DefaultParameterService extends Service {
}
class DestructuredParameterService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'destructuredParameter', { namespace: 'invalid-destructure' })
readonly typertRemote = bindTypertRemote(this, 'destructuredParameter', { namespace: 'invalid-destructure' })
constructor(ctx: Context) {
super(ctx, 'destructuredParameter')
@@ -221,7 +221,7 @@ class DestructuredParameterService extends Service {
}
class RestParameterService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'restParameter', { namespace: 'invalid-rest' })
readonly typertRemote = bindTypertRemote(this, 'restParameter', { namespace: 'invalid-rest' })
constructor(ctx: Context) {
super(ctx, 'restParameter')
@@ -234,7 +234,7 @@ class RestParameterService extends Service {
}
class NonFinalSignalService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'nonFinalSignal', { namespace: 'invalid-signal' })
readonly typertRemote = bindTypertRemote(this, 'nonFinalSignal', { namespace: 'invalid-signal' })
constructor(ctx: Context) {
super(ctx, 'nonFinalSignal')
@@ -247,7 +247,7 @@ class NonFinalSignalService extends Service {
}
class WrongBindingService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'notWrongBinding', { namespace: 'wrong-binding' })
readonly typertRemote = bindTypertRemote(this, 'notWrongBinding', { namespace: 'wrong-binding' })
constructor(ctx: Context) {
super(ctx, 'wrongBinding')
@@ -260,7 +260,7 @@ class WrongBindingService extends Service {
}
class ExportedMethodService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'exportedMethod', { namespace: 'exported' })
readonly typertRemote = bindTypertRemote(this, 'exportedMethod', { namespace: 'exported' })
constructor(ctx: Context) {
super(ctx, 'exportedMethod')
@@ -273,7 +273,7 @@ class ExportedMethodService extends Service {
}
class EmptyMethodService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'emptyMethod', { namespace: 'empty' })
readonly typertRemote = bindTypertRemote(this, 'emptyMethod', { namespace: 'empty' })
constructor(ctx: Context) {
super(ctx, 'emptyMethod')
@@ -286,7 +286,7 @@ class EmptyMethodService extends Service {
}
class CollidingWireService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'collidingWire', { namespace: 'colliding-wire' })
readonly typertRemote = bindTypertRemote(this, 'collidingWire', { namespace: 'colliding-wire' })
constructor(ctx: Context) {
super(ctx, 'collidingWire')
@@ -299,7 +299,7 @@ class CollidingWireService extends Service {
}
class ContextWireService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'contextWire', { namespace: 'context-wire' })
readonly typertRemote = bindTypertRemote(this, 'contextWire', { namespace: 'context-wire' })
constructor(ctx: Context) {
super(ctx, 'contextWire')
@@ -322,14 +322,14 @@ class NoBindingService extends Service {
}
class ObservedClaimService extends Service {
private readonly binding = bindTypeRTGateway(this, 'observedClaim', { namespace: 'observed-claim' })
private readonly binding = bindTypertRemote(this, 'observedClaim', { namespace: 'observed-claim' })
bindingReads = 0
constructor(ctx: Context) {
super(ctx, 'observedClaim')
}
get typertGateway() {
get typertRemote() {
this.bindingReads += 1
return this.binding
}
@@ -341,7 +341,7 @@ class ObservedClaimService extends Service {
}
class MissingMethodService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'missingMethod', { namespace: 'missing-method' })
readonly typertRemote = bindTypertRemote(this, 'missingMethod', { namespace: 'missing-method' })
constructor(ctx: Context) {
super(ctx, 'missingMethod')
@@ -354,7 +354,7 @@ class MissingMethodService extends Service {
}
class InheritedMethodBase extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'inheritedMethod', { namespace: 'inherited' })
readonly typertRemote = bindTypertRemote(this, 'inheritedMethod', { namespace: 'inherited' })
constructor(ctx: Context) {
super(ctx, 'inheritedMethod')
@@ -546,7 +546,7 @@ describe('TypertGatewayService', () => {
it('preserves a Host Context policy rejection for the active RPC adapter', async () => {
const { ctx } = await setup()
const rejection = new TypeRTLookupFailure({ code: 'agent-busy', message: 'owned', details: { reason: 'subagent' } })
const rejection = new TypertLookupFailure({ code: 'agent-busy', message: 'owned', details: { reason: 'subagent' } })
ctx.typert.contexts.registerHost('gatewayFixture', {
...contextProvider(ctx.extend()),
resolve: async () => { throw rejection },
@@ -892,10 +892,10 @@ describe('TypertGatewayService', () => {
}), 'binding-invalid')
const plain: {
typertGateway?: ReturnType<typeof bindTypeRTGateway>
typertRemote?: ReturnType<typeof bindTypertRemote>
run(value: string): string
} = { run: value => value }
plain.typertGateway = bindTypeRTGateway(plain, 'plainRemote', { namespace: 'plain' })
plain.typertRemote = bindTypertRemote(plain, 'plainRemote', { namespace: 'plain' })
ctx.provide('plainRemote', plain)
ctx.typert.register({
package: '@fixture/plain',
@@ -1060,7 +1060,7 @@ describe('TypertGatewayService', () => {
}
ctx.typert.lookups.register('gatewayFixture', {
...agentLookup({ id: 'agent-1' }),
resolve: () => { throw new TypeRTLookupFailure(failure) },
resolve: () => { throw new TypertLookupFailure(failure) },
})
const handler = rawConnection(ctx).handler
if (handler === undefined) throw new Error('fixture Connection did not retain the /api interceptor')
@@ -1103,7 +1103,7 @@ describe('TypertGatewayService', () => {
it('dispatches claimed invocations through /api and leaves unclaimed endpoints to its fallback', async () => {
const ctx = new Context().extend({ fixtureScope: 'http-caller' })
const routes: WebRoute[] = []
ctx.provide('httpServer', fakeHttpServer(routes) as HttpServerService)
ctx.provide('webServer', fakeHttpServer(routes) as WebServer)
const connectionFiber = ctx.plugin({ inject: [...connectionInject], apply: applyConnection })
await connectionFiber
await ctx.plugin(TypertRegistry)
@@ -1242,7 +1242,7 @@ function registerAgentLookup(ctx: Context, agent: FixtureAgent): () => Promise<v
return ctx.typert.lookups.register('gatewayFixture', agentLookup(agent))
}
function agentLookup(agent: FixtureAgent): TypeRTLookupProvider<FixtureAgent, string> {
function agentLookup(agent: FixtureAgent): TypertLookupProvider<FixtureAgent, string> {
return {
parameter: 'agent',
wire: 'agentId',

View File

@@ -16,7 +16,7 @@
"path": "../../client/connection/tsconfig.client.json"
},
{
"path": "../../typert/type-meta"
"path": "../../typert/protocol"
}
]
}

View File

@@ -18,13 +18,13 @@
"path": "../../../vendor/cordis"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../client/connection/tsconfig.host.json"
},
{
"path": "../../typert/type-meta"
"path": "../../typert/protocol"
}
]
}

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/api/remotes/README.md
README.md: 288c63c9f43654dfec428a6a8955dc537efe81a6
README.zh.md: 1fd599b08ecc1b4ae1dba738ce8946aa4eab5946
README.md: 18d39c6e86f114d2aac2f24e5d15c13335eab020
README.zh.md: bf3dfbbaa6e0c7bd1da8398977837d4cb19d4688

View File

@@ -4,9 +4,9 @@ English | [中文](README.zh.md)
Two-sided BFF for Host Remote capabilities selected by this application. The Host entry owns Agent/Session identity policy; the Client entry imports generated `/remote` artifacts as runtime values, mounts each contribution through `ctx.remote.$mount()`, and re-exports their declaration merges. Client business packages depend on this facade rather than the Gateway implementation or individual Remote runtime entries.
`createApiRemoteAgentResolver()` reuses live Agents, resumes ordinary cold sessions, deduplicates concurrent resumes, preserves the subagent ownership fence, and configures the same resolver for TypeRT `agent` and `session` lookups. The standard Web API Proxy supplies its Agent defaults and scope setup, then uses the returned resolver for legacy methods, so migrated and unmigrated methods share one policy implementation.
`createApiRemoteAgentResolver()` reuses live Agents, resumes ordinary cold sessions, deduplicates concurrent resumes, preserves the subagent ownership fence, and configures the same resolver for Typert `agent` and `session` lookups. The standard Web API Proxy supplies its Agent defaults and scope setup, then uses the returned resolver for legacy methods, so migrated and unmigrated methods share one policy implementation.
The current Client assembly mounts the Goal Remote contribution and the read-only Host plugin inventory contribution (`pluginInventory/list`). Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, and cancellation. The Client entry consumes the shared `TypeRTClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
The current Client assembly mounts the Goal Remote contribution and the read-only Host plugin inventory contribution (`pluginInventory/list`). Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
This package contains no transport or Host service discovery logic. Its Client face can be reused by Web or a future TUI that provides the same React-free `ctx.remote` contract.
@@ -14,11 +14,11 @@ This package contains no transport or Host service discovery logic. Its Client f
`src/remote-events.ts` holds `API_REMOTE_FORWARDED_EVENTS`, the allowlist of Host cordis events this application forwards to consumers verbatim — no projection, no redaction, no renaming — and therefore the legal key set of `ctx.remote.$on`; the type-only `src/types.ts` derives its selection face. Forwarding one more event is an entry in that array and nothing else: the type projection, the consumer key face, and the Host forwarding loop all derive from it.
The listener signature is not restated here. Each allowlisted event's cordis `Events` declaration lives in its owner package's client-safe `./types` export (`dsh-agent-presets`, `dsh-commands`, `dsh-credentials`, `dsh-llm`, `dsh-settings`), and both faces of this package pull those declarations in, so "forwarded verbatim" holds by construction rather than by proof. The Host face additionally asserts the list against `TypeRTForwardableEvent`, which rejects a name that is not a declared event, one that binds an AgentScope, and one whose shape is not one-way.
The listener signature is not restated here. Each allowlisted event's cordis `Events` declaration lives in its owner package's client-safe `./types` export (`dsh-agent-presets`, `dsh-commands`, `dsh-credentials`, `dsh-llm`, `dsh-settings`), and both faces of this package pull those declarations in, so "forwarded verbatim" holds by construction rather than by proof. The Host face additionally asserts the list against `TypertForwardableEvent`, which rejects a name that is not a declared event, one that binds an AgentScope, and one whose shape is not one-way.
## Build boundary
An ordinary repository package belongs to one TypeScript face: Host packages are registered in the root `tsconfig.host.json`, and Client packages in the root `tsconfig.client.json`. `api-remotes` is the only deliberate exception because its Host entry must participate in the Host TypeRT graph, while `src/client/index.ts` cannot compile until Host tsdown has generated the business packages' `/remote` declarations.
An ordinary repository package belongs to one TypeScript face: Host packages are registered in the root `tsconfig.host.json`, and Client packages in the root `tsconfig.client.json`. `api-remotes` is the only deliberate exception because its Host entry must participate in the Host Typert graph, while `src/client/index.ts` cannot compile until Host tsdown has generated the business packages' `/remote` declarations.
This package's root `tsconfig.json` is only a solution that references `tsconfig.host.json` and `tsconfig.client.json`. The Host aggregate and direct Host consumers reference the former, while the Client aggregate and direct Client consumers reference the latter; the package-root solution must not enter either aggregate's dependency graph. The two projects own disjoint source files and `.tsbuildinfo` files but share the `lib/types` output directory, with one deliberate exception: `src/remote-events.ts` and `src/types.ts` are listed in BOTH faces' `files`, because the forwarded-event allowlist is the single control point over what a consumer can receive, and the Host forwarding loop and the Client `ctx.remote.$on` key face must read one declaration rather than two that could drift.

View File

@@ -4,9 +4,9 @@
为本应用选定的 Host Remote 能力提供双侧 BFF。Host 入口负责 Agent/Session 身份策略Client 入口以运行时值形式导入生成的 `/remote` 产物,通过 `ctx.remote.$mount()` 挂载每项贡献并重新导出对应的声明合并。Client 业务包依赖该外观,而不依赖 Gateway 实现或单独的 Remote 运行时入口。
`createApiRemoteAgentResolver()` 会复用 live Agent、恢复普通冷会话、对并发恢复去重、保留 subagent ownership fence并为 TypeRT `agent``session` lookup 配置同一个 resolver。标准 Web API Proxy 提供 Agent 默认值和 scope 设置,再将返回的 resolver 用于旧方法,使已迁移与未迁移方法共用同一份策略实现。
`createApiRemoteAgentResolver()` 会复用 live Agent、恢复普通冷会话、对并发恢复去重、保留 subagent ownership fence并为 Typert `agent``session` lookup 配置同一个 resolver。标准 Web API Proxy 提供 Agent 默认值和 scope 设置,再将返回的 resolver 用于旧方法,使已迁移与未迁移方法共用同一份策略实现。
当前 Client 组合挂载 Goal Remote 贡献和只读 Host 插件清单贡献(`pluginInventory/list`。该组合卸载时Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用与取消。Client 入口通过 Cordis 消费共享的 `TypeRTClientRemote` 接口,不导入具体 Gateway它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
当前 Client 组合挂载 Goal Remote 贡献和只读 Host 插件清单贡献(`pluginInventory/list`。该组合卸载时Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
本包不包含传输逻辑或 Host 服务发现逻辑。Web 或未来的 TUI 只要提供同一份不依赖 React 的 `ctx.remote` 约定,均可复用其 Client face。
@@ -14,11 +14,11 @@
`src/remote-events.ts` 持有 `API_REMOTE_FORWARDED_EVENTS`——本应用原样转发给消费端的 Host cordis 事件名单(无投影、无脱敏、无改名),它同时就是 `ctx.remote.$on` 的合法键集;只含类型的 `src/types.ts` 派生其选择面。多转发一个事件只需在该数组里加一行:类型投影、消费端键面与 Host 转发循环全部由它派生。
监听器签名不在此处重写。名单内每条事件的 cordis `Events` 声明都住在其 owner 包 client-safe 的 `./types` 出口(`dsh-agent-presets``dsh-commands``dsh-credentials``dsh-llm``dsh-settings`),本包两个 face 都把那些声明纳入编译面因此「原样转发」是构造性成立的不需要另立证明。Host face 还额外把名单断言给 `TypeRTForwardableEvent`:未声明的事件名、绑定 AgentScope 的事件、以及形状不是单向的事件都会在此被拒绝。
监听器签名不在此处重写。名单内每条事件的 cordis `Events` 声明都住在其 owner 包 client-safe 的 `./types` 出口(`dsh-agent-presets``dsh-commands``dsh-credentials``dsh-llm``dsh-settings`),本包两个 face 都把那些声明纳入编译面因此「原样转发」是构造性成立的不需要另立证明。Host face 还额外把名单断言给 `TypertForwardableEvent`:未声明的事件名、绑定 AgentScope 的事件、以及形状不是单向的事件都会在此被拒绝。
## 构建边界
仓库中的普通包只属于一个 TypeScript faceHost 包登记在根 `tsconfig.host.json`Client 包登记在根 `tsconfig.client.json``api-remotes` 是唯一刻意拆分的特例,因为它的 Host 入口要参与 Host TypeRT 图,而 `src/client/index.ts` 必须等 Host tsdown 生成业务包的 `/remote` 声明后才能编译。
仓库中的普通包只属于一个 TypeScript faceHost 包登记在根 `tsconfig.host.json`Client 包登记在根 `tsconfig.client.json``api-remotes` 是唯一刻意拆分的特例,因为它的 Host 入口要参与 Host Typert 图,而 `src/client/index.ts` 必须等 Host tsdown 生成业务包的 `/remote` 声明后才能编译。
本包根 `tsconfig.json` 只是引用 `tsconfig.host.json``tsconfig.client.json` 的 solution。Host aggregate 和 Host 直接消费方引用前者Client aggregate 和 Client 直接消费方引用后者;禁止把包根 solution 放进任一 aggregate 的依赖图。两个 project 拥有互不重叠的源码和 `.tsbuildinfo`,但共享 `lib/types` 输出目录——只有一处刻意的例外:`src/remote-events.ts``src/types.ts` **同时**列进两个 face 的 `files`因为转发事件名单是「消费端能收到什么」的唯一控制点Host 转发循环与 Client 的 `ctx.remote.$on` 键面必须读同一份声明,而不是两份可能彼此漂移的声明。

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-remotes",
"description": "Remote BFF assembly and Host Agent/Session lookup policy",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},
@@ -55,7 +55,7 @@
"lib/types/**/*.d.ts"
],
"dependencies": {
"@deepseek-ai/dsh-type-meta": "workspace:^"
"@deepseek-ai/dsh-typert-protocol": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^",
@@ -64,6 +64,7 @@
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",
@@ -80,6 +81,7 @@
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",

View File

@@ -4,7 +4,7 @@ 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'
import { TypeRTLookupFailure } from '@deepseek-ai/dsh-type-meta'
import { TypertLookupFailure } from '@deepseek-ai/dsh-typert-protocol'
import type {} from '@deepseek-ai/dsh-typert-registry'
/** Caller-facing failures preserved by the Gateway's RPC adapter. */
@@ -111,12 +111,12 @@ export async function inspectApiRemoteSession(
}
/**
* Create the Host's shared Agent resolver and configure Agent/Session TypeRT lookups.
* Create the Host's shared Agent resolver and configure Agent/Session Typert lookups.
* Live Agents are reused, ordinary cold sessions resume once per identity, and
* subagent-owned identities retain the legacy `agent-busy` fence.
* @param ctx - owning Host Context.
* @param options - defaults and Agent-scope setup used only for cold resume.
* @returns resolver shared by legacy API Proxy methods and TypeRT lookups.
* @returns resolver shared by legacy API Proxy methods and Typert lookups.
*/
export function createApiRemoteAgentResolver(
ctx: Context,
@@ -199,7 +199,7 @@ export function createApiRemoteAgentResolver(
ctx.inject(['typert'], (typeCtx) => {
const resolveAgent = async (sessionId: SessionId): Promise<Agent> => {
const found = await agentFor(sessionId)
if ('error' in found) throw new TypeRTLookupFailure(found.error)
if ('error' in found) throw new TypertLookupFailure(found.error)
return found.agent
}
typeCtx.typert.lookups.configure('agent', resolveAgent)

View File

@@ -3,23 +3,25 @@
import type { Context } from '@deepseek-ai/cordis'
import commandsRemote from '@deepseek-ai/dsh-commands/remote'
import goalsRemote from '@deepseek-ai/dsh-goal/remote'
import dynamicRemote from '@deepseek-ai/dsh-cordis-host-runner/remote'
import pluginInventoryRemote from '@deepseek-ai/dsh-host-plugin-inventory/remote'
import messageFeedbackRemote from '@deepseek-ai/dsh-message-feedback/remote'
import type { TypeRTClientRemote } from '@deepseek-ai/dsh-type-meta'
import type { TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol'
export type { TypeRTClientRemote as ClientRemote } from '@deepseek-ai/dsh-type-meta'
export type { TypertClientRemote as ClientRemote } from '@deepseek-ai/dsh-typert-protocol'
export type { PluginInventorySnapshot } from '@deepseek-ai/dsh-host-plugin-inventory/types'
export type {} from '@deepseek-ai/dsh-commands/remote'
export type {} from '@deepseek-ai/dsh-goal/remote'
export type {} from '@deepseek-ai/dsh-host-plugin-inventory/remote'
export type {} from '@deepseek-ai/dsh-message-feedback/remote'
// The forwarded-event allowlist's selection seat: without it in the consumer's
// compilation face `TypeRTRemoteEvent` is `never` and every `$on` call fails.
// compilation face `TypertRemoteEvent` is `never` and every `$on` call fails.
export type { ApiRemoteForwardedEvent } from '../types.ts'
// The owner packages' client-safe `./types` exports supply the `Events`
// signatures `$on` hands to a listener, so a consumer reads the very
// declaration the Host emits rather than a flattened restatement of it.
export type {} from '@deepseek-ai/dsh-commands/types'
export type {} from '@deepseek-ai/dsh-cordis-host-runner/types'
export type {} from '@deepseek-ai/dsh-credentials/types'
export type {} from '@deepseek-ai/dsh-llm/types'
export type {} from '@deepseek-ai/dsh-agent-presets/types'
@@ -37,14 +39,58 @@ export type {
MuxFrame, PromptContentPart, QuestionResponsePayload, QueueAction, RpcError, RpcId, RpcReceipt,
RpcRequest, RpcResponse, RpcResult, SessionId, SessionModels, SessionSearchItem,
SessionSummary, SettingsNamespaceView, SettingsPathOpView, SkillEntry, StreamChunk,
SubagentAddress, SubagentCatalog, TaskView, ToolCallView, ToolEventView, ToolResultView,
SubagentAddress, SubagentCatalog, JobView, ToolCallView, ToolEventView, ToolResultView,
WorkspaceId, WorkspaceView,
} from '@deepseek-ai/dsh-client-connection/client'
export type {} from '@deepseek-ai/dsh-api-gateway/client'
export type {} from '@deepseek-ai/dsh-cordis-host-runner/remote'
// The payload vocabulary of the selected namespaces, re-exported so a Client
// contribution can name what it sends and receives without importing a Host
// package: this assembly is the one place both planes legitimately meet.
export type {
ApprovalRequestId,
CordisHalfState,
CordisDynamicPackageId,
CordisDynamicPluginId,
CordisDynamicPluginRunId,
CordisDynamicRunMode,
CordisInspectMethodManifest,
CordisInspectPlatform,
CordisInspectProviderManifest,
CordisInspectProviderView,
CordisInspectQueryRequest,
CordisInspectQueryResolution,
CordisInspectQueryResolved,
CordisInspectRequestId,
CordisInspectResolveAck,
CordisRunDiagnostic,
CordisRunStatus,
DynamicCordisClientSource,
DynamicCordisHostHalfResult,
DynamicCordisInventoryRow,
DynamicCordisInvokeResult,
DynamicCordisPackage,
DynamicCordisRequestResolved,
DynamicCordisResolveAck,
DynamicCordisRetracted,
DynamicCordisRunRequest,
DynamicCordisRunResolution,
DynamicCordisRunAttempt,
DynamicCordisRunResponse,
DynamicCordisStopResponse,
DynamicCordisUndefineReceipt,
RequestRunOutcome,
} from '@deepseek-ai/dsh-cordis-host-runner/types'
// The JSON vocabulary those payloads are built from, re-exported for the same
// reason: a Client contribution names what it sends without importing a Host
// package, and this assembly is where both planes legitimately meet.
export type { JsonValue } from '@deepseek-ai/dsh-session/types'
declare module '@deepseek-ai/cordis' {
interface Context {
/** Generated Remote namespaces selected by this Client assembly. */
remote: TypeRTClientRemote
remote: TypertClientRemote
}
}
@@ -59,13 +105,17 @@ export const inject = ['remote']
export async function apply(ctx: Context): Promise<() => Promise<void>> {
const disposers: Array<() => Promise<void>> = []
try {
for (const contribution of [commandsRemote, goalsRemote, pluginInventoryRemote, messageFeedbackRemote]) {
for (const contribution of [
commandsRemote, goalsRemote, dynamicRemote, pluginInventoryRemote, messageFeedbackRemote,
]) {
disposers.push(await ctx.remote.$mount(contribution))
}
} catch (error) {
for (const dispose of disposers.reverse()) await dispose()
throw error
}
// Unwound in reverse mount order, so a namespace never outlives one mounted
// after it.
return async () => {
for (const dispose of disposers.reverse()) await dispose()
}

View File

@@ -1,6 +1,6 @@
/** Host BFF entry and Loader shell for the Remote contribution assembly. */
import type { TypeRTForwardableEvent } from '@deepseek-ai/dsh-type-meta'
import type { TypertForwardableEvent } from '@deepseek-ai/dsh-typert-protocol'
import { API_REMOTE_FORWARDED_EVENTS } from './remote-events.ts'
// The owner packages' client-safe `./types` exports carry the cordis `Events`
@@ -8,6 +8,7 @@ import { API_REMOTE_FORWARDED_EVENTS } from './remote-events.ts'
// makes the shape assertion below judge real signatures rather than an empty
// event vocabulary.
import type {} from '@deepseek-ai/dsh-commands/types'
import type {} from '@deepseek-ai/dsh-cordis-host-runner/types'
import type {} from '@deepseek-ai/dsh-credentials/types'
import type {} from '@deepseek-ai/dsh-llm/types'
import type {} from '@deepseek-ai/dsh-agent-presets/types'
@@ -37,7 +38,7 @@ export type { ApiRemoteForwardedEvent } from './types.ts'
// and every entry is ONE-WAY (a waterfall or bail shape returns something other
// than void and is excluded). Widening the array to an event that fails any of
// these fails here, not on the wire.
API_REMOTE_FORWARDED_EVENTS satisfies readonly TypeRTForwardableEvent[]
API_REMOTE_FORWARDED_EVENTS satisfies readonly TypertForwardableEvent[]
/** Host plugin body; the selected contributions mount only in Client environments. */
export function apply(): void {}

View File

@@ -11,7 +11,7 @@ export const name = 'api-remotes-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/** No runtime invariant: TypeRT and the Agent/Session registries own the observed relationships. */
/** No runtime invariant: Typert and the Agent/Session registries own the observed relationships. */
const install: InvariantInstaller = () => {}
/**

View File

@@ -18,6 +18,12 @@ export const API_REMOTE_FORWARDED_EVENTS = [
'agent-preset/selected',
'commands/change',
'credentials/updated',
'cordis/request-run',
'cordis/request-run-resolved',
'cordis/dynamic-package',
'cordis/dynamic-retract',
'cordis/inspect-query',
'cordis/inspect-query-resolved',
'llm/adapters-updated',
'settings/document-updated',
] as const

View File

@@ -14,6 +14,6 @@ import type { API_REMOTE_FORWARDED_EVENTS } from './remote-events.ts'
/** Type projection of the allowlist; the consumer and the Host read this one. */
export type ApiRemoteForwardedEvent = typeof API_REMOTE_FORWARDED_EVENTS[number]
declare module '@deepseek-ai/dsh-type-meta' {
interface TypeRTRemoteEventSelection extends Record<ApiRemoteForwardedEvent, true> {}
declare module '@deepseek-ai/dsh-typert-protocol' {
interface TypertRemoteEventSelection extends Record<ApiRemoteForwardedEvent, true> {}
}

View File

@@ -5,7 +5,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import { createApiRemoteAgentResolver } from '@deepseek-ai/dsh-api-remotes'
import { TypeRTLookupFailure } from '@deepseek-ai/dsh-type-meta'
import { TypertLookupFailure } from '@deepseek-ai/dsh-typert-protocol'
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
const sid = (value: string): SessionId => value as SessionId
@@ -147,7 +147,7 @@ describe('API Remote Agent resolver races', () => {
if (provider === undefined) throw new Error('Agent Host Context provider was not mounted')
const resolution = provider.resolve(sessionId)
await expect(resolution).rejects.toBeInstanceOf(TypeRTLookupFailure)
await expect(resolution).rejects.toBeInstanceOf(TypertLookupFailure)
await expect(resolution).rejects.toMatchObject({ failure: { code: 'agent-busy' } })
await ctx.fiber.dispose()
})

View File

@@ -51,7 +51,7 @@ describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
const { Context } = cordis
const { default: AgentRegistry } = await import(urls.agent)
const connectionHost = await import(urls.connectionHost)
const { default: TypertGatewayService } = await import(urls.apiGatewayHost)
const { default: TypertRemoteService } = await import(urls.apiGatewayHost)
const { default: GoalService } = await import(urls.goal)
const { TYPERT } = await import(urls.goalTypert)
const { default: TypertRegistry } = await import(urls.registryHost)
@@ -59,7 +59,7 @@ describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
const routes = []
const host = new Context()
host.provide('httpServer', {
host.provide('webServer', {
register(route) {
routes.push(route)
return () => { routes.splice(routes.indexOf(route), 1) }
@@ -70,7 +70,7 @@ describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
await host.plugin({ inject: connectionHost.inject, apply: connectionHost.apply })
await host.plugin(TypertRegistry)
await host.plugin(AgentRegistry)
await host.plugin(TypertGatewayService)
await host.plugin(TypertRemoteService)
await host.plugin(GoalService)
host.typert.register(TYPERT)

View File

@@ -22,6 +22,10 @@
},
{
"path": "../../credentials/credentials"
},
{
"path": "../../extensions/cordis-host-runner"
},
{
"path": "../../goal/goal"
@@ -45,7 +49,7 @@
"path": "../../settings/settings"
},
{
"path": "../../typert/type-meta"
"path": "../../typert/protocol"
}
]
}

View File

@@ -37,17 +37,20 @@
{
"path": "../../session/session-persistence"
},
{
"path": "../../extensions/cordis-host-runner"
},
{
"path": "../../settings/settings"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../typert/registry"
},
{
"path": "../../typert/type-meta"
"path": "../../typert/protocol"
}
]
}

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-attachment-local",
"description": "Private content-addressed DSH_HOME attachment storage",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},
@@ -24,7 +24,7 @@
"peerDependencies": {
"@deepseek-ai/dsh-attachment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-home-paths": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
@@ -34,7 +34,7 @@
"devDependencies": {
"@deepseek-ai/dsh-attachment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-home-paths": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -5,7 +5,7 @@ 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'
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
import { readImageFile, saveImageFile, validateImageFile } from './store.ts'
export { detectImage } from './image.ts'

View File

@@ -6,7 +6,7 @@
{ "path": "../../../vendor/cosmokit" },
{ "path": "../../../vendor/cordis" },
{ "path": "../attachment" },
{ "path": "../../util/paths" },
{ "path": "../../support/invariants" }
{ "path": "../../util/home-paths" },
{ "path": "../../runtime-diagnostics/invariants" }
]
}

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-attachment",
"description": "Durable immutable attachment storage seam for the DeepSeek Harness",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},

View File

@@ -6,6 +6,6 @@
{ "path": "../../../vendor/cosmokit" },
{ "path": "../../../vendor/cordis" },
{ "path": "../../util/brand" },
{ "path": "../../support/invariants" }
{ "path": "../../runtime-diagnostics/invariants" }
]
}

View File

@@ -1,51 +0,0 @@
# @deepseek-ai/dsh-bash-env
English | [中文](README.zh.md)
The tool-independent shell environment plugin: owns the `ctx.bashEnv` registry of trusted, per-execution `DSH_*` variables that the model-facing shell tools (`dsh-tool-bash`, `dsh-tool-pwsh`) collect into every shell call's environment. Built-in shell facts (`DSH_HOME`, `DSH_SHELL=1`, `DSH_SESSION_ID`) are owned by the registry itself; other plugins register additional enumerable facts with effect-scoped disposal, and duplicate ownership or undeclared runtime keys fail loudly.
The package root exports the Cordis plugin contract (`name`, `inject`, `Config`, `apply`) plus the `BashEnvRegistry` service class and its contributor types; consumers use `ctx.bashEnv` after loading this plugin.
## Config
```yaml
- id: bash-env
name: '@deepseek-ai/dsh-bash-env'
config:
dshHome: C:\Users\me\.dsh # default: $DSH_HOME, then ~/.dsh
```
## Managed environment
Every foreground and background model shell call receives a newly collected trusted `DSH_*` environment. `DSH_HOME` is the absolute Harness home resolved by [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) (`dshHome` config, then ambient `$DSH_HOME`, then `~/.dsh`) and `DSH_SHELL=1` identifies the managed child. Agent calls additionally receive `DSH_SESSION_ID=agent.session.header.id`; when the active persistence seam locates a JSONL artifact they also receive `DSH_SESSION_JSONL=<absolute target path>`. The JSONL path is a location hint: it may not exist before the first flush or contain the current buffered turn, and it is not an authorization credential.
`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 '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-bash-env'
export const inject = ['bashEnv']
export function apply(ctx: Context): void {
ctx.bashEnv.register({
name: 'deployment-region',
variables: { DSH_DEPLOYMENT_REGION: { description: 'Current deployment region.' } },
resolve: execution => execution.agent === undefined ? {} : { DSH_DEPLOYMENT_REGION: 'cn-north' },
})
}
```
The overlay is computed from the current `ToolExecution` and passed through the dedicated `BashExecRequest.dshEnv` channel. The local executors remove all inherited `DSH_*` before merging that snapshot, so nested harnesses and concurrent parent/child agents cannot leak stale identities. `process.env` is never modified. The shell tools' descriptions teach the generic `$DSH_*` convention rather than naming persistence-specific variables or adding a permanent system-prompt section.
## Model Experience
Indirectly, through the shell tools (`dsh-tool-bash`, `dsh-tool-pwsh`), which collect this registry's managed `DSH_*` snapshot into every shell-tool call.
#### KV Cache effect
No direct invalidation; the named consumers own any request-prefix changes.
## Known Limitations and Deferred Work
- **`list()` enumerates contributor-declared variables only** — registry-owned built-ins (`DSH_HOME`, `DSH_SHELL`, `DSH_SESSION_ID`) are not included, so diagnostics, prompt, or UI code must not treat `list()` as an exhaustive environment catalog.

View File

@@ -1,51 +0,0 @@
# @deepseek-ai/dsh-bash-env
[English](README.md) | 中文
工具无关的 shell 环境插件:拥有 `ctx.bashEnv` 注册表,管理受信任的、每次执行收集的 `DSH_*` 变量,供模型可见的 shell 工具(`dsh-tool-bash``dsh-tool-pwsh`)收集进每次 shell 调用的环境。内置 shell 事实(`DSH_HOME``DSH_SHELL=1``DSH_SESSION_ID`归注册表自身所有其他插件可以注册额外的可枚举事实注册随插件纤维fiber释放重复所有权或未声明的运行时键会响亮失败。
包根导出 Cordis 插件约定(`name``inject``Config``apply`)以及 `BashEnvRegistry` 服务类及其 contributor 类型;消费方在加载本插件后使用 `ctx.bashEnv`
## Config
```yaml
- id: bash-env
name: '@deepseek-ai/dsh-bash-env'
config:
dshHome: C:\Users\me\.dsh # default: $DSH_HOME, then ~/.dsh
```
## Managed environment
每次前台与后台模型 shell 调用都会收到一份新收集的受信任 `DSH_*` 环境。`DSH_HOME` 是由 [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) 解析的 Harness 主目录绝对路径(`dshHome` 配置,然后环境变量 `$DSH_HOME`,然后 `~/.dsh``DSH_SHELL=1` 标识受管理的子进程。带 agent智能体的调用额外收到 `DSH_SESSION_ID=agent.session.header.id`;当活动的持久化 seam 定位到 JSONL 工件时,它们还会收到 `DSH_SESSION_JSONL=<绝对目标路径>`。JSONL 路径只是位置提示:首次 flush 之前它可能不存在,也不一定包含当前缓冲中的轮次,并且它不是授权凭据。
`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 '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-bash-env'
export const inject = ['bashEnv']
export function apply(ctx: Context): void {
ctx.bashEnv.register({
name: 'deployment-region',
variables: { DSH_DEPLOYMENT_REGION: { description: 'Current deployment region.' } },
resolve: execution => execution.agent === undefined ? {} : { DSH_DEPLOYMENT_REGION: 'cn-north' },
})
}
```
覆盖层根据当前 `ToolExecution` 计算,并通过专用的 `BashExecRequest.dshEnv` 通道传递。本地执行器在合并该快照前移除所有继承的 `DSH_*`,因此嵌套 harness 与并发的父子 agent 无法泄漏陈旧身份。`process.env` 永不被修改。shell 工具的描述只教授通用的 `$DSH_*` 约定,而不是点名持久化相关的变量或添加常驻的 system-prompt 段落。
## Model Experience
通过 shell 工具(`dsh-tool-bash``dsh-tool-pwsh`)间接产生影响;这些工具会把该注册表的受管 `DSH_*` 快照收集进每次 shell 工具调用。
#### KV Cache effect
不会直接导致缓存失效;任何请求前缀变更均由上述消费方负责。
## Known Limitations and Deferred Work
- **`list()` 只枚举 contributor 声明的变量** — 注册表自有的内置键(`DSH_HOME``DSH_SHELL``DSH_SESSION_ID`不包含在内因此诊断、prompt 或 UI 代码不得把 `list()` 当作完整的环境目录。

View File

@@ -1,53 +0,0 @@
# @deepseek-ai/dsh-bash
[English](README.md) | 中文
**`BashExecutor`**`ctx.bash`)定义 bash 后端做什么即运行前台命令与启动后台进程但不规定如何实现。task id、所有权、收集、取消与通知属于通用 `ctx.tasks` 运行时。
本包承担 bash 能力的 Service Definition 角色,各角色因此可以独立演进(和替换):
| 包 | 职责 |
|---|---|
| `@deepseek-ai/dsh-bash`(本包) | Service Definition抽象服务 + 词汇类型 |
| `@deepseek-ai/dsh-bash-local` | Service provider本地子进程 |
| `@deepseek-ai/dsh-bash-sandbox` | Service provider沿用 `dsh-bash-local` 的机制,但通过 [`ctx.sandbox`](../../sandbox/sandbox/) 限制每次 spawn并将拒绝报告为结果事实 |
| `@deepseek-ai/dsh-tool-bash` | 基于 `ctx.bash`、面向模型的工具 schema |
该拆分是一个标准的能力 seam[capability-seams Agent Note](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)`dsh-bash-sandbox` 是位于同一 Service Definition 之后的沙箱执行器——Consumer 检测其 `sandboxMode` 能力并添加升权字段,无需导入提供方——容器化或远程执行器也可以同样接入。
## 服务 API`ctx.bash`
| 成员 | 语义 |
|---|---|
| `run(spec)` | 前台执行。命令完成时 resolve。**只会因基础设施失败而 reject**工作目录不可用、shell 缺失、信号已在调用前中止);非零退出、超时终止和中止导致的终止都会 resolve 为描述性 `BashRunResult`。 |
| `start(spec)` | 后台执行。立即返回不含任务语义的 `BashProcess` 句柄;**不应用超时**。调用方可以将其适配到 `ctx.tasks`。 |
| `sandboxMode` | 工具层的能力事实:沙箱执行器用于限制执行的默认模式(基类中为 `undefined`,即「此执行器不使用沙箱」)。`dsh-tool-bash` 会在注册时读取它,仅当组合确实支持升权字段时才公布这些字段。 |
| `BashProcess.readOutput()` | **增量** 读取输出:连续读取绝不会重复交付。因缓冲区容量限制而丢失数据的读取会标记 `lossy`,并指向完整流 spill 文件。 |
| `BashProcess.kill()` | 终止进程组。如果进程已结束,返回 `false`。 |
实现会继承 `BashExecutor` 并实现抽象方法。dispose资源释放必须终止每个运行中的进程并等待其退出。
`BASH_SETTINGS_NAMESPACE``bash`)由此处导出而非由某个提供方导出,因为它命名的是能力而不是实现。一个宿主只组装一个 `ctx.bash` 提供方——win32 层会把 POSIX 行换成 pwsh 行,同时挂载两者会因服务重复注册而在加载期失败——所以每个提供方都能用自己的 schema 与组装条目注册这同一个命名空间,两者永不相撞;在平台间携带的 `settings.yaml` 也能在两边继续解析。
## 词汇
`BashExecRequest`command、workdir?、timeoutMs?、stdoutMaxBytes?、signal?、stdin?、env?、dshEnv?、sandboxPolicy?)在执行前解析为 `BashExecSpec`command、workdir、timeoutMs、stdoutMaxBytes、signal?、stdin?、env?、dshEnv?、sandboxPolicy`stdoutMaxBytes` 是受信任前台运行的捕获预算,用于必须解析完整有界 stdout 的消费方;面向模型的 bash 工具不公开该字段。`sandboxPolicy` 在请求上可选,在已解析 spec 上必填但可为 null它携带完整的每次调用模式与工作区根目录。沙箱工具路径通过 `ctx.sandboxPolicy` 从调用会话解析它;沙箱执行器的直接调用方回退到部署策略,非沙箱执行器则携带该字段但不作限制。
每会话沙箱模式覆盖词汇(`'sandbox/mode'` 事件、`effectiveSandboxMode(events)` fold 以及 `setSandboxMode(session, mode)` 写入路径)不位于此处。它是所有强制执行家族共享的策略状态,属于 [`@deepseek-ai/dsh-sandbox-policy`](../../sandbox/sandbox-policy/)。`run()` 返回 `BashRunResult``start()` 返回 `BashProcess`,其增量读取与终止方法由 `dsh-tool-bash` 适配为通用任务注册。沙箱执行器会在前台结果与已结算进程句柄上标记 `BashSandboxInfo`。详见 `src/types.ts` 与 [subsystems/bash.md](../../../docs/subsystems/bash.md)。
`stdin` 与普通 `env` 由同进程插件hooks 桥接、原生插件)设置,用于向 hook 命令提供其 JSON payload 和 `CLAUDE_PROJECT_DIR``CLAUDE_PLUGIN_ROOT` 值。`dshEnv` 是受类型限制、仅允许受管 key 的独立受信任 overlay导出的 `DSH_ENV_PREFIX` 是该 namespace、其 `DshEnvironmentKey` 模板类型、执行器清理、注册表验证、派生内置名称与模型指引的统一来源。模型 bash 使用 `ctx.bashEnv` 收集的当前快照。实现会移除继承的受管 key再在普通 `env` 之后合并 `dshEnv`,因此省略的当前事实不会回退到陈旧环境状态,`env` 条目也无法顶掉受管值。面向模型的工具不将这三者中的任何一个公开为参数。这三者在已解析 spec 上仍然可选缺失表示没有输入overlay。详见 [bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md) 与 [会话环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
导出的 `parseExitStatus`(连同 `ParsedExitStatus`)是 shell 工具共享渲染约定的另一半:`dsh-tool-bash``renderResult``dsh-tool-pwsh``renderPwshResult` 追加的 `[exit code: N]``[killed by signal: X]` marker 的逆解析。两个工具的 `presentResult` 都用它把渲染文本拆成 terminal 卡的输出正文与其退出状态 pill它放在 Service Definition 中,两个工具便永远不会在 marker 约定上漂移。
## 模型体验
通过 `dsh-tool-bash` 间接影响;该工具会将执行器输出与沙箱事实转为指引和保留的工具结果 token。
#### KV Cache 影响
不会直接导致 KV Cache 失效;请求前缀变更由具名消费方负责。
## 已知限制与暂缓事项
- **没有交互式输入词汇**`stdin` 只会在 spawn 时写入一次并关闭seam 不提供向运行中任务继续输入的通道,也没有 PTY 会话概念。
- **前台超时始终由执行器负责**seam 上由调用方负责 deadline 的模式已由 [工具调用超时策略 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md) 明确暂缓。

View File

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

View File

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

View File

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

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: 4c82aa749edbeada0a344b0b119d1644544d9732
README.zh.md: bf86c52b1ccf0dcdc8c3acc5057c22ddd304e715
README.md: 80f8e8a694b32eb8a1499f75a56de852f7106643
README.zh.md: c8396625f2f59008d71b865f35a04ba55bb313cd

View File

@@ -35,7 +35,7 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](..
## Profiles
A profile is a directory under `$DSH_HOME/profiles/<name>` (the Harness home resolves through [`resolveDshHome`](../../util/paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list — and the user's own `cordis.patch.yml`. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory — one symlink per package the installation's app and bundles depend on — so bare plugin names in any profile resolve through Node's ordinary parent-walk without pnpm managing in-box packages. `PROFILE_TEMPLATES` (`web`, `headless`) auto-initialize on first use; other names fail loud until `initProfile` creates them (the `dsh plugin` path). `loadProfile` normalizes an exact installation-owned bundle tuple to its shipped template while preserving every other manifest field; any extra, missing, or reordered entry makes the list user-owned and leaves it unchanged.
A profile is a directory under `$DSH_HOME/profiles/<name>` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list — and the user's own `cordis.patch.yml`. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory — one symlink per package the installation's app and bundles depend on — so bare plugin names in any profile resolve through Node's ordinary parent-walk without pnpm managing in-box packages. `PROFILE_TEMPLATES` (`web`, `headless`) auto-initialize on first use; other names fail loud until `initProfile` creates them (the `dsh plugin` path). `loadProfile` normalizes an exact installation-owned bundle tuple to its shipped template while preserving every other manifest field; any extra, missing, or reordered entry makes the list user-owned and leaves it unchanged.
User-level machine-local preferences also live in the Harness home:

View File

@@ -35,7 +35,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面
## Profiles
profile 是位于 `$DSH_HOME/profiles/<name>` 下的目录harness home 由 [`resolveDshHome`](../../util/paths/README.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和用户自己的 `cordis.patch.yml`。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录(安装目录的应用与各组合包依赖的每个包对应一个符号链接),使任意 profile 中的裸插件名都能经 Node 常规的逐级向上查找解析,而无需由 pnpm 管理随安装内置的包。`PROFILE_TEMPLATES``web``headless`)在首次使用时自动初始化;其他名称在 `initProfile` 创建之前都会明确报错(即 `dsh plugin` 路径)。`loadProfile` 会将与安装自有组合包元组完全一致的列表规范化为随发行版交付的模板,同时保留 manifest 中其他所有字段;一旦条目有任何额外、缺失或重排,该列表就归用户所有并保持不变。
profile 是位于 `$DSH_HOME/profiles/<name>` 下的目录harness home 由 [`resolveDshHome`](../../util/home-paths/README.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和用户自己的 `cordis.patch.yml`。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录(安装目录的应用与各组合包依赖的每个包对应一个符号链接),使任意 profile 中的裸插件名都能经 Node 常规的逐级向上查找解析,而无需由 pnpm 管理随安装内置的包。`PROFILE_TEMPLATES``web``headless`)在首次使用时自动初始化;其他名称在 `initProfile` 创建之前都会明确报错(即 `dsh plugin` 路径)。`loadProfile` 会将与安装自有组合包元组完全一致的列表规范化为随发行版交付的模板,同时保留 manifest 中其他所有字段;一旦条目有任何额外、缺失或重排,该列表就归用户所有并保持不变。
用户级的机器本地偏好同样位于 harness home 中:

View File

@@ -1,7 +1,7 @@
{
"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-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},
@@ -39,9 +39,9 @@
"@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-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-home-paths": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
@@ -56,9 +56,9 @@
"@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-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-home-paths": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@types/js-yaml": "^4.0.9",
"@deepseek-ai/cordis": "workspace:^"

View File

@@ -15,8 +15,8 @@ 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 { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-home-paths'
import { createLaunchEnvironmentSnapshot, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
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'
@@ -177,7 +177,7 @@ function readEnvLayer(
export function loadLayeredEnv(
binName: string, cwd: string = process.cwd(),
warn: (line: string) => void = line => void process.stderr.write(line),
): EnvironmentSnapshot {
): LaunchEnvironmentSnapshot {
const home = resolveDshHome()
const inherited = { ...process.env } as Record<string, string>
// Parse both layers first: a rejection must not leave one file applied.
@@ -190,7 +190,7 @@ export function loadLayeredEnv(
if (process.env[name] === undefined) process.env[name] = value
}
}
return createEnvironmentSnapshot([
return createLaunchEnvironmentSnapshot([
{ source: 'process', values: inherited },
...project === undefined ? [] : [{ source: 'project-env' as const, path: project.path, values: project.values }],
...user === undefined ? [] : [{ source: 'user-env' as const, path: user.path, values: user.values }],
@@ -665,7 +665,7 @@ export function assertEntriesLoaded(ctx: Context, binName: string): void {
/**
* Value mirrors used because Cordis's const enum has no runtime object to import.
* Keep aligned with `packages/self-modification/tool-cordis/src/fiber-state.ts` and
* Keep aligned with `packages/extensions/tool-cordis/src/fiber-state.ts` and
* `packages/client/web/src/loader-status.ts`.
*/
const FIBER_PENDING = 0 as FiberState.PENDING

View File

@@ -29,7 +29,7 @@ import {
import { basename, dirname, join } from 'node:path'
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 { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
import { loadOverlayPatches } from './index.ts'
/** Directory under the Harness home holding every profile. */
@@ -208,7 +208,7 @@ function ensureSymlink(link: string, target: string): void {
* resolves without pnpm ever managing it — the exact "bundles come from the
* installation" contract. The closure (not just direct dependencies) is
* required for out-of-tree plugins: their peer dependencies name Service
* Definition packages (`dsh-compact`, `dsh-invariants`, ...) that the app
* Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app
* reaches only through its Service provider packages. Symlinked packages
* resolve their own dependencies from their real directories (Node's default
* symlink-following), so each package needs only its one flat link.
@@ -231,7 +231,7 @@ export function healProfilesModuleFallback(installAnchor: string, home: string =
const queue: { anchor: string; manifest: ProfileManifest }[] = [{ anchor: installAnchor, manifest: appManifest }]
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
// Peer dependencies participate: Service Definition packages (dsh-subprocess,
// dsh-compact, ...) are peers of their implementations, never plain
// dsh-compaction, ...) are peers of their implementations, never plain
// dependencies, yet out-of-tree plugins import them directly.
/* v8 ignore next -- a real app manifest always declares dependencies */
for (const dep of [...Object.keys(next.manifest.dependencies ?? {}), ...Object.keys(next.manifest.peerDependencies ?? {})]) {

View File

@@ -796,7 +796,7 @@ describe('addHarnessSourceSection', () => {
expect(rendered).toContain(EXPECTED)
// Harness-owned opener (-100) → source (-99) → persona (0). The >= 0 guards
// keep a drifted opener/persona string from a false pass through `-1 < n`.
const identityAt = rendered.indexOf('You are an AI agent powered by the DeepSeek Harness SDK.')
const identityAt = rendered.indexOf('You are an AI agent powered by DeepSeek Harness.')
const sourceAt = rendered.indexOf(EXPECTED)
const personaAt = rendered.indexOf('You are a coding agent.')
expect(identityAt).toBeGreaterThanOrEqual(0)

View File

@@ -24,16 +24,16 @@
"path": "../../../vendor/hmr"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../core/system-prompt"
},
{
"path": "../../util/environment"
"path": "../../util/launch-environment"
},
{
"path": "../../util/paths"
"path": "../../util/home-paths"
}
]
}

View File

@@ -1,7 +1,7 @@
{
"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.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},

View File

@@ -18,7 +18,7 @@
"path": "../../../vendor/loader"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -44,7 +44,7 @@
maxTitleBytes: 80
- id: session-title-llm
name: '@deepseek-ai/dsh-session-title-first-message-llm'
name: '@deepseek-ai/dsh-session-title-first-prompt-llm'
config:
targetWords: 5
targetCjkCharacters: 10
@@ -52,8 +52,8 @@
maxOutputTokens: 64
timeoutMs: 60000
- id: user-interaction
name: '@deepseek-ai/dsh-user-interaction'
- id: user-questions
name: '@deepseek-ai/dsh-user-questions'
- id: agent
name: '@deepseek-ai/dsh-agent'
@@ -66,8 +66,8 @@
provider: deepseek-official
model: deepseek-v4-flash
- id: tasks
name: '@deepseek-ai/dsh-tasks-local'
- id: jobs
name: '@deepseek-ai/dsh-jobs-local'
- id: llm-retry
name: '@deepseek-ai/dsh-llm-retry'
@@ -76,7 +76,7 @@
# `llm-deepseek:` or `llm-pi-ai:` section there overrides the adapter entries
# below without a restart, and is what the web Models page writes.
- id: settings
name: '@deepseek-ai/dsh-settings-local'
name: '@deepseek-ai/dsh-settings-file'
# Credential sources: inherited environment over the managed
# `$DSH_HOME/.credentials.yaml`, with project and user `.env` fallbacks.
@@ -122,13 +122,13 @@
# Session telemetry is mounted but disabled by default. DSH_TELEMETRY_MODE
# explicitly opts into FULL or FEEDBACK_ONLY reporting; uploading mirrors
# session-log records onto OTLP/HTTP logs with no telemetry/record redaction
# session-log records onto OTLP/HTTP logs with no session-telemetry/record redaction
# rule, so exports are the raw captured copy. The deployment stance, env
# seams, and follow-ups are pinned in the default-off Agent Note.
# DSH_TELEMETRY_OTLP_URL overrides the production endpoint. A non-empty
# DSH_TELEMETRY_DISABLED — any value, including '0'/'false' — opts the
# process out (the launchers patch the row disabled; config cannot disable
# a row). Exports carry the harness home's anonymous user id ($DSH_HOME/.userid,
# a row). Exports carry the harness home's anonymous user id ($DSH_HOME/.anonymous-user-id,
# random UUID; delete the file to reset the identity) as the Resource's
# user.id. The exporter/processor values normally bound the shutdown drain
# to ~1s against an unreachable collector: exporter.timeoutMillis is both
@@ -139,7 +139,7 @@
# shutdownTimeoutMillis is the load-bearing outer bound when a transport
# promise never settles. Every CLI exit path drains it by disposing the root
# on SIGINT/SIGTERM.
- id: telemetry-otel
- id: session-telemetry-otel
name: '@deepseek-ai/dsh-session-telemetry-otel'
config:
mode: !!js process.env.DSH_TELEMETRY_MODE || 'DISABLED'
@@ -185,7 +185,7 @@
policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'workspace-write') === 'danger-full-access' ? 'never' : 'ask'"
- id: permission
name: '@deepseek-ai/dsh-permission'
name: '@deepseek-ai/dsh-permission-presets'
config:
presets:
read-only:
@@ -198,8 +198,8 @@
sandbox: danger-full-access
approval: never
- id: bash-env
name: '@deepseek-ai/dsh-bash-env'
- id: shell-env
name: '@deepseek-ai/dsh-shell-env'
- id: tool-bash
name: '@deepseek-ai/dsh-tool-bash'
@@ -209,11 +209,11 @@
name: '@deepseek-ai/dsh-tool-pwsh'
disabled: !!js process.platform !== 'win32'
- id: tool-tasks
name: '@deepseek-ai/dsh-tool-tasks'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
- id: fs-policy
name: '@deepseek-ai/dsh-fs-policy'
- id: fs-observation-policy
name: '@deepseek-ai/dsh-fs-observation-policy'
- id: tool-fs
name: '@deepseek-ai/dsh-tool-fs'
@@ -223,16 +223,16 @@
config:
sampleOverCapGlobResults: false
- id: workspace-context
name: '@deepseek-ai/dsh-workspace-context'
- id: agent-instructions
name: '@deepseek-ai/dsh-agent-instructions'
config:
maxBytes: 65536
- id: skill
name: '@deepseek-ai/dsh-skill'
- id: skill-local
name: '@deepseek-ai/dsh-skill-local'
- id: skill-filesystem
name: '@deepseek-ai/dsh-skill-filesystem'
- id: skill-badge
name: '@deepseek-ai/dsh-skill-badge'
@@ -250,8 +250,8 @@
- id: goal
name: '@deepseek-ai/dsh-goal'
- id: goal-session
name: '@deepseek-ai/dsh-goal-session'
- id: goal-round-driver
name: '@deepseek-ai/dsh-goal-round-driver'
- id: command-goal
name: '@deepseek-ai/dsh-command-goal'
@@ -275,8 +275,8 @@
- id: token-meter
name: '@deepseek-ai/dsh-token-meter'
- id: compact-basic
name: '@deepseek-ai/dsh-compact-basic'
- id: compaction-basic
name: '@deepseek-ai/dsh-compaction-basic'
# Human `/compact`: one useful reduction below the automatic threshold. Backend
# independent, so it follows whichever compaction service this leaf mounts.
@@ -286,13 +286,13 @@
- id: subagent
name: '@deepseek-ai/dsh-subagent'
- id: subagent-spawn
name: '@deepseek-ai/dsh-subagent-spawn'
- id: subagent-spawn-in-process
name: '@deepseek-ai/dsh-subagent-spawn-in-process'
config:
providerName: spawn
- id: subagent-fork
name: '@deepseek-ai/dsh-subagent-fork'
- id: subagent-fork-in-process
name: '@deepseek-ai/dsh-subagent-fork-in-process'
config:
providerName: fork
@@ -335,8 +335,8 @@
- id: tool-subagent-report
name: '@deepseek-ai/dsh-tool-subagent-report'
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
- id: workflow-worker-thread
name: '@deepseek-ai/dsh-workflow-worker-thread'
config:
provider: spawn
@@ -344,7 +344,7 @@
name: '@deepseek-ai/dsh-tool-workflow'
- id: timeout-policy
name: '@deepseek-ai/dsh-timeout-policy'
name: '@deepseek-ai/dsh-tool-call-timeout-policy'
- id: spill-local
name: '@deepseek-ai/dsh-spill-local'
@@ -360,8 +360,8 @@
# Compacts oversized tool results before the broader conversation compactor
# runs, preserving the model-visible result within the configured budget.
- id: tool-result-prune
name: '@deepseek-ai/dsh-compact-tool-result-prune'
- id: tool-result-pruner
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
config:
thresholdChars: 8192
headChars: 4096
@@ -390,8 +390,8 @@
maxOutputChars: 16000
# Consecutive-repeat reminders on the tool chain.
- id: repeat-tool-guard
name: '@deepseek-ai/dsh-repeat-tool-guard'
- id: repeat-tool-reminder
name: '@deepseek-ai/dsh-repeat-tool-reminder'
config:
thresholds: [3, 5, 8]
argumentsPreviewChars: 500

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-base",
"description": "The shared dsh core as a profile bundle: every profile's first patch layer, inserting the base plugin rows over the empty profile root",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},
@@ -45,29 +45,29 @@
"@deepseek-ai/dsh-agent-default-model": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-attachment-local": "workspace:^",
"@deepseek-ai/dsh-bash-env": "workspace:^",
"@deepseek-ai/dsh-shell-env": "workspace:^",
"@deepseek-ai/dsh-bash-sandbox": "workspace:^",
"@deepseek-ai/dsh-command-compact": "workspace:^",
"@deepseek-ai/dsh-command-feedback": "workspace:^",
"@deepseek-ai/dsh-command-goal": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-compact-basic": "workspace:^",
"@deepseek-ai/dsh-compact-tool-result-prune": "workspace:^",
"@deepseek-ai/dsh-compaction-basic": "workspace:^",
"@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:^",
"@deepseek-ai/dsh-credentials-local": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-fs-policy": "workspace:^",
"@deepseek-ai/dsh-fs-observation-policy": "workspace:^",
"@deepseek-ai/dsh-fs-sandbox": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-goal-session": "workspace:^",
"@deepseek-ai/dsh-goal-round-driver": "workspace:^",
"@deepseek-ai/dsh-api-gateway": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
"@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
"@deepseek-ai/dsh-llm-retry": "workspace:^",
"@deepseek-ai/dsh-permission": "workspace:^",
"@deepseek-ai/dsh-permission-presets": "workspace:^",
"@deepseek-ai/dsh-plan-mode": "workspace:^",
"@deepseek-ai/dsh-pwsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-repeat-tool-guard": "workspace:^",
"@deepseek-ai/dsh-repeat-tool-reminder": "workspace:^",
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
@@ -77,22 +77,22 @@
"@deepseek-ai/dsh-session-query-sqlite": "workspace:^",
"@deepseek-ai/dsh-session-telemetry-otel": "workspace:^",
"@deepseek-ai/dsh-session-title": "workspace:^",
"@deepseek-ai/dsh-session-title-first-message-llm": "workspace:^",
"@deepseek-ai/dsh-settings-local": "workspace:^",
"@deepseek-ai/dsh-session-title-first-prompt-llm": "workspace:^",
"@deepseek-ai/dsh-settings-file": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-badge": "workspace:^",
"@deepseek-ai/dsh-skill-local": "workspace:^",
"@deepseek-ai/dsh-skill-filesystem": "workspace:^",
"@deepseek-ai/dsh-spill-local": "workspace:^",
"@deepseek-ai/dsh-spill-policy": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-claude-code": "workspace:^",
"@deepseek-ai/dsh-subagent-codex": "workspace:^",
"@deepseek-ai/dsh-subagent-fork": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-subagent-fork-in-process": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tasks-local": "workspace:^",
"@deepseek-ai/dsh-timeout-policy": "workspace:^",
"@deepseek-ai/dsh-jobs-local": "workspace:^",
"@deepseek-ai/dsh-tool-call-timeout-policy": "workspace:^",
"@deepseek-ai/dsh-token-meter": "workspace:^",
"@deepseek-ai/dsh-tool-bash": "workspace:^",
"@deepseek-ai/dsh-tool-fs": "workspace:^",
@@ -105,7 +105,7 @@
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
"@deepseek-ai/dsh-tool-subagent-control": "workspace:^",
"@deepseek-ai/dsh-tool-subagent-report": "workspace:^",
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
"@deepseek-ai/dsh-tool-jobs": "workspace:^",
"@deepseek-ai/dsh-tool-todo": "workspace:^",
"@deepseek-ai/dsh-tool-web": "workspace:^",
"@deepseek-ai/dsh-tool-workflow": "workspace:^",
@@ -113,11 +113,11 @@
"@deepseek-ai/dsh-typert-loader": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/dsh-user-interaction": "workspace:^",
"@deepseek-ai/dsh-user-questions": "workspace:^",
"@deepseek-ai/dsh-web": "workspace:^",
"@deepseek-ai/dsh-web-search-deepseek": "workspace:^",
"@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
"@deepseek-ai/dsh-workspace-context": "workspace:^"
"@deepseek-ai/dsh-workflow-worker-thread": "workspace:^",
"@deepseek-ai/dsh-agent-instructions": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",

View File

@@ -32,7 +32,7 @@ describe('dsh-base bundle', () => {
)
expect(rows.length).toBeGreaterThan(50)
expect(rows.some(row => row.id === 'agent-loop')).toBe(true)
expect(rows.find(row => row.id === 'telemetry-otel')?.config?.['mode']).toEqual({
expect(rows.find(row => row.id === 'session-telemetry-otel')?.config?.['mode']).toEqual({
__jsExpr: "process.env.DSH_TELEMETRY_MODE || 'DISABLED'",
})
expect(rows.filter(row => row.id === 'subagent-codex')).toHaveLength(1)

View File

@@ -12,7 +12,7 @@
"path": "../../../vendor/cordis"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -22,7 +22,7 @@
- insert:
# Code Mode is a core execution capability, not a Web component.
- id: code-runtime
name: '@deepseek-ai/dsh-code-runtime-worker'
name: '@deepseek-ai/dsh-code-runtime-worker-thread'
- id: headless-startup
name: '@deepseek-ai/dsh-headless/startup'

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-headless",
"description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},
@@ -45,7 +45,7 @@
},
"dependencies": {
"@deepseek-ai/dsh-cmdline": "workspace:^",
"@deepseek-ai/dsh-code-runtime-worker": "workspace:^",
"@deepseek-ai/dsh-code-runtime-worker-thread": "workspace:^",
"@deepseek-ai/schemastery": "workspace:^",
"commander": "^15.0.0"
},

View File

@@ -4,7 +4,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
import type { Agent, AgentHandle, CreateAgentOptions } from '@deepseek-ai/dsh-agent'
import AgentDefaultModelService from '@deepseek-ai/dsh-agent-default-model'
import AgentDefaultModelConfig from '@deepseek-ai/dsh-agent-default-model'
import { createAssistantMessage } from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
@@ -55,7 +55,7 @@ async function bench(script: Script): Promise<{
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentDefaultModelService, { provider: 'test-provider', model: 'test-model' })
await ctx.plugin(AgentDefaultModelConfig, { provider: 'test-provider', model: 'test-model' })
ctx.agents.setFactory({
async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle> {
const session = ctx.sessions.create(options.sessionId, {

View File

@@ -30,7 +30,7 @@
"path": "../../core/session"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../boot/cmdline"

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/bundle/web-app/README.md
README.md: 06856a47cd8ccc2c6ee5a53c40928b1bd2933cc7
README.zh.md: 8befc7c7404ea1b082842f122769967fff32df2f
README.md: 3f7fbe7bbbeba2ee753ab2dfa8c945e798dd6a02
README.zh.md: 357ca9946ed7fef1bf4c691c6666156bb293b5e0

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides over [`dsh-base`](../base/README.md): it sets the coding persona, inserts the Web host rows (webserver, API gateway, workspace, projection cache, storage) and the browser plugin roster, the always-on client-plugin reload chain ([`dsh-client-hmr`](../../client/hmr/README.md), idle until a rebuild watcher rewrites client bundles), and mounts this package's `web-runtime` glue plugin (config `{printUrl, surfaceContext, trustedHosts}`). That plugin resolves the built frontend dist through `@deepseek-ai/dsh-frontend`'s exports, samples bind-dependent LAN trust once, provides it as `webRuntime` to the browser-trust fence and client roster, mounts the [`frontend-static`](../../host/frontend-static/README.md) fallback owner, registers the harness-source and web-surface prompt sections plus the bash-visible `DSH_WEB_URL` runtime variable when `surfaceContext` is true, and prints the `dsh web:` URL line when `printUrl` is true, after its Loader tree settles so a sibling failure cannot announce a dead app. This bundle also owns the app command line: the ordinary `web-startup` provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), parses `--host`, `--port`, repeatable `--trusted-host`, and the app's `--help`, then provides `webStartup`. Flag-configured rows inject that service and read it directly from lazy config, so nothing binds a port before argument resolution and `dsh --profile web --help` starts no server. [`dsh-headless`](../headless/README.md) is a sibling surface over the same base and does not mount this bundle.
The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides over [`dsh-base`](../base/README.md): it sets the coding persona, inserts the Web host rows (webserver, API gateway, workspace, projection cache, storage) and the browser plugin roster, the always-on client-plugin reload chain ([`dsh-client-hmr`](../../client/hmr/README.md), idle until a rebuild watcher rewrites client bundles), and mounts this package's `web-runtime` glue plugin (config `{printUrl, surfaceContext, trustedHosts}`). That plugin resolves the built frontend dist through `@deepseek-ai/dsh-web-frontend`'s exports, samples bind-dependent LAN trust once, provides it as `webRuntime` to the browser-trust fence and client roster, mounts the [`frontend-static`](../../host/frontend-static/README.md) fallback owner, registers the harness-source and web-surface prompt sections plus the bash-visible `DSH_WEB_URL` runtime variable when `surfaceContext` is true, and prints the `dsh web:` URL line when `printUrl` is true, after its Loader tree settles so a sibling failure cannot announce a dead app. This bundle also owns the app command line: the ordinary `web-startup` provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), parses `--host`, `--port`, repeatable `--trusted-host`, and the app's `--help`, then provides `webStartup`. Flag-configured rows inject that service and read it directly from lazy config, so nothing binds a port before argument resolution and `dsh --profile web --help` starts no server. [`dsh-headless`](../headless/README.md) is a sibling surface over the same base and does not mount this bundle.
## Model Experience

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在 [`dsh-base`](../base/README.md) 之上:设置 coding persona插入 Web 宿主行webserver、API 网关、workspace、投影缓存、存储、浏览器插件名录与始终挂载的客户端插件重载链[`dsh-client-hmr`](../../client/hmr/README.md),在重建 watcher 改写客户端 bundle 之前保持空闲),并挂载本包的 `web-runtime` 粘合插件(配置为 `{printUrl, surfaceContext, trustedHosts}`)。该插件通过 `@deepseek-ai/dsh-frontend` 的 exports 解析已构建的前端 dist只采样一次依赖 bind 的 LAN 信任信息并将其作为 `webRuntime` 提供给浏览器信任栅栏和客户端名录,挂载 [`frontend-static`](../../host/frontend-static/README.md) 回退席位所有者,在 `surfaceContext` 为 true 时注册 Harness 源码与 Web 表层提示词段落,以及 bash 可见的 `DSH_WEB_URL` 运行时变量,并在 `printUrl` 为 true 时等自身的 Loader 配置树结算后再打印 `dsh web:` URL 行,避免兄弟行失败时公告一个已失效的应用。本组合包还持有应用命令行:普通 `web-startup` 提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`[`dsh-cmdline`](../../boot/cmdline/README.md)),解析 `--host``--port`、可重复的 `--trusted-host` 以及应用自己的 `--help`,再提供 `webStartup`。由 flag 配置的行会注入该服务,并在惰性配置中直接读取它,因此参数解析完成前不会有任何东西绑定端口,`dsh --profile web --help` 也不会启动服务器。[`dsh-headless`](../headless/README.md) 是同一 base 之上的同级表层,不挂载本组合包。
dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在 [`dsh-base`](../base/README.md) 之上:设置 coding persona插入 Web 宿主行webserver、API 网关、workspace、投影缓存、存储、浏览器插件名录与始终挂载的客户端插件重载链[`dsh-client-hmr`](../../client/hmr/README.md),在重建 watcher 改写客户端 bundle 之前保持空闲),并挂载本包的 `web-runtime` 粘合插件(配置为 `{printUrl, surfaceContext, trustedHosts}`)。该插件通过 `@deepseek-ai/dsh-web-frontend` 的 exports 解析已构建的前端 dist只采样一次依赖 bind 的 LAN 信任信息并将其作为 `webRuntime` 提供给浏览器信任栅栏和客户端名录,挂载 [`frontend-static`](../../host/frontend-static/README.md) 回退席位所有者,在 `surfaceContext` 为 true 时注册 Harness 源码与 Web 表层提示词段落,以及 bash 可见的 `DSH_WEB_URL` 运行时变量,并在 `printUrl` 为 true 时等自身的 Loader 配置树结算后再打印 `dsh web:` URL 行,避免兄弟行失败时公告一个已失效的应用。本组合包还持有应用命令行:普通 `web-startup` 提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`[`dsh-cmdline`](../../boot/cmdline/README.md)),解析 `--host``--port`、可重复的 `--trusted-host` 以及应用自己的 `--help`,再提供 `webStartup`。由 flag 配置的行会注入该服务,并在惰性配置中直接读取它,因此参数解析完成前不会有任何东西绑定端口,`dsh --profile web --help` 也不会启动服务器。[`dsh-headless`](../headless/README.md) 是同一 base 之上的同级表层,不挂载本组合包。
## 模型体验

View File

@@ -34,7 +34,7 @@
- id: tools
config:
# TEMPORARY workaround: DSH_TOOLS_MODE (native|code|both) opts a whole dsh
# process into Code Mode while per-session tool-mode selection is being
# process into Code Mode while per-session tool-presentation selection is being
# designed; unset keeps the schema default (native). Remove the env seam
# once the web UI owns the choice per session.
mode: !!js process.env.DSH_TOOLS_MODE
@@ -45,7 +45,7 @@
# window.__DSH_BOOT__; the modules row is simultaneously a host row.
- insert:
- id: code-runtime
name: '@deepseek-ai/dsh-code-runtime-worker'
name: '@deepseek-ai/dsh-code-runtime-worker-thread'
- id: storage
name: '@deepseek-ai/dsh-storage'
@@ -66,8 +66,8 @@
maxNoteBytes: 8192
# Browser Session export: `/export` command plus the shared download dialog.
- id: session-export
name: '@deepseek-ai/dsh-session-export'
- id: session-log-download
name: '@deepseek-ai/dsh-session-log-download'
- id: workspace
name: '@deepseek-ai/dsh-workspace'
@@ -98,6 +98,9 @@
- id: api-gateway
name: '@deepseek-ai/dsh-host-apiproxy'
- id: cordis-host-runner
name: '@deepseek-ai/dsh-cordis-host-runner'
# Ordinary provider for the parsed Web flags. Its plugin-level injection
# waits for cmdlineArgs; no launcher metadata or special row kind is needed.
- id: web-startup
@@ -164,6 +167,9 @@
- id: client-runtime
name: '@deepseek-ai/dsh-client-runtime'
- id: cordis-client-runner
name: '@deepseek-ai/dsh-cordis-client-runner'
- id: ui-theme
name: '@deepseek-ai/dsh-client-ui-theme'
@@ -182,11 +188,11 @@
- id: ui-settings-general
name: '@deepseek-ai/dsh-client-ui-settings-general'
- id: ui-models
name: '@deepseek-ai/dsh-client-ui-models'
- id: ui-settings-models
name: '@deepseek-ai/dsh-client-ui-settings-models'
- id: ui-plugins
name: '@deepseek-ai/dsh-client-ui-plugins'
- id: ui-settings-plugin-inventory
name: '@deepseek-ai/dsh-client-ui-settings-plugin-inventory'
- id: ui-conversation
name: '@deepseek-ai/dsh-client-ui-conversation'
@@ -195,6 +201,9 @@
- id: ui-tool
name: '@deepseek-ai/dsh-client-ui-tool'
- id: ui-cordis
name: '@deepseek-ai/dsh-client-ui-cordis'
# Durable workflow lifecycle as an independent Chat node after the
# existing generic workflow tool row.
- id: ui-workflow-run
@@ -209,13 +218,13 @@
- id: ui-workspace
name: '@deepseek-ai/dsh-client-ui-workspace'
# Input triggers: the '/' | '@' pipeline (ui-slash), the command surface over
# it (ui-command), and the two reference sources (ui-skill / ui-subagent).
- id: ui-slash
name: '@deepseek-ai/dsh-client-ui-slash'
# Input triggers: the '/' | '@' pipeline (ui-input-trigger), the command surface over
# it (ui-commands), and the two reference sources (ui-skill / ui-subagent).
- id: ui-input-trigger
name: '@deepseek-ai/dsh-client-ui-input-trigger'
- id: ui-command
name: '@deepseek-ai/dsh-client-ui-command'
- id: ui-commands
name: '@deepseek-ai/dsh-client-ui-commands'
- id: ui-skill
name: '@deepseek-ai/dsh-client-ui-skill'
@@ -223,9 +232,9 @@
- id: ui-subagent
name: '@deepseek-ai/dsh-client-ui-subagent'
# Background tasks: the session-header list over the tasksBySession mirror.
- id: ui-task
name: '@deepseek-ai/dsh-client-ui-task'
# Background jobs: the session-header list over the jobsBySession mirror.
- id: ui-jobs
name: '@deepseek-ai/dsh-client-ui-jobs'
# Goal surface: GoalBar in the input dock over the goal session projection.
- id: ui-goal
@@ -233,15 +242,15 @@
# Per-message feedback: Like/Dislike plus an optional note in the
# assistant-message action strip, over the messageFeedback Remote.
- id: ui-feedback
name: '@deepseek-ai/dsh-client-ui-feedback'
- id: ui-message-feedback
name: '@deepseek-ai/dsh-client-ui-message-feedback'
# Model selection: the /model popupSelect + composer seat over session.models.
- id: ui-model
name: '@deepseek-ai/dsh-client-ui-model'
- id: ui-model-selection
name: '@deepseek-ai/dsh-client-ui-model-selection'
- id: ui-permission
name: '@deepseek-ai/dsh-client-ui-permission'
name: '@deepseek-ai/dsh-client-ui-permission-presets'
# The agent-preset row in General settings: the default preset for
# sessions created later. Absent a roster it renders nothing.
@@ -250,15 +259,15 @@
# Plugin configuration: the host-plane sections a user owns, as expandable
# cards. A namespace this deployment does not expose renders nothing.
- id: ui-plugin-config
name: '@deepseek-ai/dsh-client-ui-plugin-config'
- id: ui-settings-plugins
name: '@deepseek-ai/dsh-client-ui-settings-plugins'
# Plan control: the composer plan seat over the plan projection + /plan channel.
- id: ui-plan
name: '@deepseek-ai/dsh-client-ui-plan'
- id: ui-question
name: '@deepseek-ai/dsh-client-ui-question'
- id: ui-user-questions
name: '@deepseek-ai/dsh-client-ui-user-questions'
- id: ui-trajectory
name: '@deepseek-ai/dsh-client-ui-trajectory'
@@ -274,7 +283,7 @@
# absent from a surface overlay would silently reappear the day someone reorders
# the composition.
# `bash-env` STAYS in the host plane: `apps/cli/src/web.ts` injects it to
# `shell-env` STAYS in the host plane: `apps/cli/src/web.ts` injects it to
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
# the criterion for host-plane ownership — injection resolves before any session
# exists, so there is no agent to key by. Behind a preset realm those variables
@@ -286,17 +295,17 @@
- id: tool-pwsh
disabled: true
# The background-task REGISTRY stays on the host plane; only the model-facing
# `task_*` controls move. Its producers — `tool-bash` here, `tool-pty` and a
# The background-job REGISTRY stays on the host plane; only the model-facing
# `job_*` controls move. Its producers — `tool-bash` here, `tool-terminal` and a
# non-continuable `tool-subagent` elsewhere — are preset rows that resolve it
# with `ctx.get`, and an entry-local realm around the registry is invisible to
# every sibling row outside that realm, so `run_in_background` answered
# "background tasks unavailable" while the controls sat in the catalog. That is
# "background jobs unavailable" while the controls sat in the catalog. That is
# the `goals` criterion read from inside the preset: a Service a row outside its
# realm READS belongs to the plane both can see. The registry is keyed by owning
# agent, so one host instance serves every session exactly as before presets.
- id: tool-tasks
- id: tool-jobs
disabled: true
- id: tool-fs
@@ -310,14 +319,14 @@
# The `skill` REGISTRY stays in the host plane. It is host+per-scope layered
# (the tools-registry shape): deployment-level providers — repository plugins,
# a host skill-local row — register into its global layer, while a preset's
# `skill-local` registers into that preset's layer, and each agent reads the
# a host skill-filesystem row — register into its global layer, while a preset's
# `skill-filesystem` registers into that preset's layer, and each agent reads the
# merged catalog its scope chain selects. Only the per-agent rows move behind
# presets: the base host `skill-local` row is disabled here (presets own local
# presets: the base host `skill-filesystem` row is disabled here (presets own local
# discovery), and `tool-skill` is what a preset mounts to give its agent the
# catalog and loader at all.
- id: skill-local
- id: skill-filesystem
disabled: true
- id: tool-skill
@@ -328,7 +337,7 @@
# domain as Remote endpoints, and a Remote method picks its receiver Service
# from a generated descriptor — it resolves `goals` on the host, so a
# per-session realm would answer `service-unavailable` for every browser call.
# That is the `bash-env` criterion read from the other side: injection is not
# That is the `shell-env` criterion read from the other side: injection is not
# the only host relationship a Service can have. The registry is keyed by
# session, so one host instance serves every session exactly as before presets.
@@ -345,13 +354,13 @@
# `tasks` and `goals`; the reasoning has one home in
# `.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.md`.
- id: compact-basic
- id: compaction-basic
disabled: true
- id: command-compact
disabled: true
- id: tool-result-prune
- id: tool-result-pruner
disabled: true
# The subagent registry and its backends STAY in the host plane. `subagents` is
@@ -379,7 +388,7 @@
# scope-aware — one copy per mounted preset means every child gets `report`
# registered once per live session, which throws on the second.
- id: workflow-workerthread
- id: workflow-worker-thread
disabled: true
- id: tool-workflow
@@ -388,7 +397,7 @@
- id: tool-ralph
disabled: true
- id: workspace-context
- id: agent-instructions
disabled: true
- id: tool-todo

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-web-app",
"description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},
@@ -53,37 +53,40 @@
"@deepseek-ai/dsh-client-modules": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^",
"@deepseek-ai/dsh-client-ui-command": "workspace:^",
"@deepseek-ai/dsh-client-ui-commands": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-ui-cordis": "workspace:^",
"@deepseek-ai/dsh-client-ui-deliverables": "workspace:^",
"@deepseek-ai/dsh-client-ui-directory-picker": "workspace:^",
"@deepseek-ai/dsh-client-ui-directory-picker-browse": "workspace:^",
"@deepseek-ai/dsh-client-ui-directory-picker-native": "workspace:^",
"@deepseek-ai/dsh-client-ui-feedback": "workspace:^",
"@deepseek-ai/dsh-client-ui-message-feedback": "workspace:^",
"@deepseek-ai/dsh-client-ui-goal": "workspace:^",
"@deepseek-ai/dsh-client-ui-layout": "workspace:^",
"@deepseek-ai/dsh-client-ui-model": "workspace:^",
"@deepseek-ai/dsh-client-ui-models": "workspace:^",
"@deepseek-ai/dsh-client-ui-plugins": "workspace:^",
"@deepseek-ai/dsh-client-ui-permission": "workspace:^",
"@deepseek-ai/dsh-client-ui-model-selection": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings-models": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-client-ui-permission-presets": "workspace:^",
"@deepseek-ai/dsh-client-ui-plan": "workspace:^",
"@deepseek-ai/dsh-client-ui-plugin-config": "workspace:^",
"@deepseek-ai/dsh-client-ui-question": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings-plugins": "workspace:^",
"@deepseek-ai/dsh-client-ui-user-questions": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings-general": "workspace:^",
"@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
"@deepseek-ai/dsh-client-ui-skill": "workspace:^",
"@deepseek-ai/dsh-client-ui-slash": "workspace:^",
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
"@deepseek-ai/dsh-client-ui-subagent": "workspace:^",
"@deepseek-ai/dsh-client-ui-task": "workspace:^",
"@deepseek-ai/dsh-client-ui-jobs": "workspace:^",
"@deepseek-ai/dsh-client-ui-theme": "workspace:^",
"@deepseek-ai/dsh-client-ui-tool": "workspace:^",
"@deepseek-ai/dsh-client-ui-workflow-run": "workspace:^",
"@deepseek-ai/dsh-client-ui-trajectory": "workspace:^",
"@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
"@deepseek-ai/dsh-cmdline": "workspace:^",
"@deepseek-ai/dsh-code-runtime-worker": "workspace:^",
"@deepseek-ai/dsh-frontend": "workspace:^",
"@deepseek-ai/dsh-frontend-static": "workspace:^",
"@deepseek-ai/dsh-code-runtime-worker-thread": "workspace:^",
"@deepseek-ai/dsh-cordis-client-runner": "workspace:^",
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
"@deepseek-ai/dsh-web-frontend": "workspace:^",
"@deepseek-ai/dsh-host-frontend-static": "workspace:^",
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
"@deepseek-ai/dsh-host-directory-picker-auto": "workspace:^",
"@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^",
@@ -92,7 +95,7 @@
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-message-feedback": "workspace:^",
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
"@deepseek-ai/dsh-session-export": "workspace:^",
"@deepseek-ai/dsh-session-log-download": "workspace:^",
"@deepseek-ai/dsh-session-stats": "workspace:^",
"@deepseek-ai/dsh-storage": "workspace:^",
"@deepseek-ai/dsh-storage-domain": "workspace:^",
@@ -103,14 +106,14 @@
},
"peerDependencies": {
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-bash-env": "workspace:^",
"@deepseek-ai/dsh-shell-env": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-bash-env": "workspace:^",
"@deepseek-ai/dsh-shell-env": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"

View File

@@ -16,11 +16,11 @@ import { fileURLToPath } from 'node:url'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot'
import * as FrontendStatic from '@deepseek-ai/dsh-frontend-static'
import * as FrontendStatic from '@deepseek-ai/dsh-host-frontend-static'
import type {} from '@deepseek-ai/cordis-plugin-loader'
import type {} from '@deepseek-ai/dsh-host-webserver'
import type {} from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-bash-env'
import type {} from '@deepseek-ai/dsh-shell-env'
/** Stable Cordis plugin name. */
export const name = 'web-app'
@@ -32,7 +32,7 @@ const SOURCE_ROOT = fileURLToPath(new URL('../../../..', import.meta.url))
const WEB_RUNTIME_SERVICE = 'webRuntime'
/** Services required before the web runtime can mount. */
export const inject = ['httpServer']
export const inject = ['webServer']
/** Plugin config: composed deployment settings plus per-invocation command-line values. */
export interface Config {
@@ -102,13 +102,13 @@ function webSurfacePrompt(webUrl: string): string {
+ updateContract
+ 'Starting another server does not update this GUI. '
+ 'The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. '
+ 'Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.'
+ 'Do not start a replacement server unless the user asks; if one is needed, use a managed background job and verify its exact URL.'
}
/** Resolve the canonical loopback URL from the active Web server. */
function localWebUrl(ctx: Context): string {
const port = ctx.get('httpServer')?.port
if (port === undefined) throw new Error('web-app: httpServer service missing while resolving Web runtime')
const port = ctx.get('webServer')?.port
if (port === undefined) throw new Error('web-app: webServer service missing while resolving Web runtime')
return `http://${LOOPBACK_HOST}:${String(port)}`
}
@@ -116,7 +116,7 @@ function localWebUrl(ctx: Context): string {
function resolveDistIndex(): string {
const require = createRequire(import.meta.url)
try {
return require.resolve('@deepseek-ai/dsh-frontend/dist/index.html')
return require.resolve('@deepseek-ai/dsh-web-frontend/dist/index.html')
} catch {
/* v8 ignore next 2 -- reachable only on a checkout without a built dist; the test tree builds it */
throw new Error('web-app: frontend dist not built; run pnpm run build from the repository root first')
@@ -129,11 +129,11 @@ export const internals: { resolveDistIndex: () => string } = { resolveDistIndex
/**
* Mount the Web runtime: dist serving, surface prompt, the bash runtime
* variable, and the URL line.
* @param ctx - plugin context carrying the httpServer service.
* @param ctx - plugin context carrying the webServer service.
* @param config - validated {@link Config}.
*/
export function apply(ctx: Context, config: Config): void {
const runtime = resolveLanTrust(ctx.httpServer.host, config.trustedHosts)
const runtime = resolveLanTrust(ctx.webServer.host, config.trustedHosts)
// Release dependent rows only after bind-dependent trust has been sampled once.
ctx.provide(WEB_RUNTIME_SERVICE, runtime)
ctx.plugin(FrontendStatic, { distIndex: internals.resolveDistIndex() })
@@ -146,8 +146,8 @@ export function apply(ctx: Context, config: Config): void {
text: () => webSurfacePrompt(localWebUrl(promptCtx)),
})
})
ctx.inject(['bashEnv'], (runtimeCtx) => {
runtimeCtx.bashEnv.register({
ctx.inject(['shellEnv'], (runtimeCtx) => {
runtimeCtx.shellEnv.register({
name: 'web-runtime',
variables: {
[DSH_WEB_URL]: { description: 'Canonical local URL of the DeepSeek Harness Web GUI serving this session.' },
@@ -164,7 +164,7 @@ export function apply(ctx: Context, config: Config): void {
const printUrl = (): void => {
// Reuse the exact LAN snapshot provided to the /api trust fence.
const lanCandidate = runtime.lanAddresses[0]
const port = ctx.httpServer.port
const port = ctx.webServer.port
console.log(`dsh web: ${localWebUrl(ctx)}${lanCandidate === undefined ? '' : ` (LAN: http://${lanCandidate}:${String(port)})`}`)
}
// This row's own activation can precede a sibling failure. The app owns
@@ -177,7 +177,7 @@ export function apply(ctx: Context, config: Config): void {
// The tree can be disposed while the boot was in flight (early
// SIGTERM); a URL line for a dead server would only mislead, and
// reading the torn-down port would turn a clean shutdown into a crash.
if (ctx.get('httpServer') !== undefined) printUrl()
if (ctx.get('webServer') !== undefined) printUrl()
// Loader reports a failed boot; this row only stays quiet.
}, () => {})
}

View File

@@ -11,7 +11,7 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import type { HttpServerService } from '@deepseek-ai/dsh-host-webserver'
import type { WebServer } from '@deepseek-ai/dsh-host-webserver'
import { apply, Config, internals } from '../src/index.ts'
vi.mock('node:os', async importOriginal => ({
@@ -43,8 +43,8 @@ function stageDist(): string {
return index
}
/** A fake httpServer capturing the fallback seat and index taps. */
function fakeHttpServer(host: '127.0.0.1' | '0.0.0.0' = '127.0.0.1'): { server: HttpServerService; seat: () => unknown } {
/** A fake webServer capturing the fallback seat and index taps. */
function fakeHttpServer(host: '127.0.0.1' | '0.0.0.0' = '127.0.0.1'): { server: WebServer; seat: () => unknown } {
let fallback: unknown
const server = {
host,
@@ -54,7 +54,7 @@ function fakeHttpServer(host: '127.0.0.1' | '0.0.0.0' = '127.0.0.1'): { server:
return () => { fallback = undefined }
},
applyIndexTaps: (html: string) => html,
} as unknown as HttpServerService
} as unknown as WebServer
return { server, seat: () => fallback }
}
@@ -74,9 +74,9 @@ describe('web-app runtime glue', () => {
stageDist()
const ctx = new Context()
const { server, seat } = fakeHttpServer('0.0.0.0')
ctx.provide('httpServer', server)
ctx.provide('webServer', server)
const contributions: BashContribution[] = []
ctx.provide('bashEnv', {
ctx.provide('shellEnv', {
register: (contribution: BashContribution) => {
contributions.push(contribution)
return () => {}
@@ -110,7 +110,7 @@ describe('web-app runtime glue', () => {
it('stays quiet with printUrl off', async () => {
stageDist()
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
ctx.provide('webServer', fakeHttpServer().server)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ctx, new Config({ printUrl: false, surfaceContext: true, trustedHosts: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
@@ -125,9 +125,9 @@ describe('web-app runtime glue', () => {
it('skips the surface context when disabled (the one-shot layer): no prompt section, no bash variables', async () => {
stageDist()
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
ctx.provide('webServer', fakeHttpServer().server)
const contributions: BashContribution[] = []
ctx.provide('bashEnv', {
ctx.provide('shellEnv', {
register: (contribution: BashContribution) => {
contributions.push(contribution)
return () => {}
@@ -146,7 +146,7 @@ describe('web-app runtime glue', () => {
it('prints the loopback-only URL line when no LAN snapshot exists', async () => {
stageDist()
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
ctx.provide('webServer', fakeHttpServer().server)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ctx, new Config({ printUrl: true, surfaceContext: true, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
@@ -159,7 +159,7 @@ describe('web-app runtime glue', () => {
// Settlement path: the line waits for loader.await() so supervisors can
// RPC immediately after observing it.
const settled = new Context()
settled.provide('httpServer', fakeHttpServer().server)
settled.provide('webServer', fakeHttpServer().server)
let release: () => void
const settlement = new Promise<void>((resolve) => { release = resolve })
provideLoader(settled, () => settlement)
@@ -176,7 +176,7 @@ describe('web-app runtime glue', () => {
// for a process that is about to exit.
log.mockClear()
const failed = new Context()
failed.provide('httpServer', fakeHttpServer().server)
failed.provide('webServer', fakeHttpServer().server)
provideLoader(failed, async () => { throw new Error('boot failed') })
apply(failed, new Config({ printUrl: true, surfaceContext: true, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
@@ -188,14 +188,14 @@ describe('web-app runtime glue', () => {
log.mockClear()
const torn = new Context()
const child = torn.plugin((childCtx: Context) => {
childCtx.provide('httpServer', fakeHttpServer().server)
childCtx.provide('webServer', fakeHttpServer().server)
})
await child
let releaseTorn: () => void
const tornSettlement = new Promise<void>((resolve) => { releaseTorn = resolve })
provideLoader(torn, () => tornSettlement)
apply(torn, new Config({ printUrl: true, surfaceContext: true, trustedHosts: [] }))
await child.dispose() // the httpServer service goes away
await child.dispose() // the webServer service goes away
releaseTorn!()
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
@@ -209,11 +209,11 @@ describe('web-app runtime glue', () => {
// section must throw, never render a URL with an undefined port.
const { server } = fakeHttpServer()
Object.defineProperty(server, 'port', { get: () => undefined })
ctx.provide('httpServer', server)
ctx.provide('webServer', server)
apply(ctx, new Config({ printUrl: false, surfaceContext: true, trustedHosts: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
await expect(ctx.systemPrompt.assemble()).rejects.toThrow('httpServer service missing')
await expect(ctx.systemPrompt.assemble()).rejects.toThrow('webServer service missing')
await ctx.fiber.dispose()
})

View File

@@ -33,10 +33,10 @@
"path": "../../core/system-prompt"
},
{
"path": "../../bash/bash-env"
"path": "../../shell/shell-env"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -89,9 +89,9 @@ If `test:gui` is red on code you did not touch, neither silently fix nor ignore
## New plugin package checklist
Bringing up a new `packages/client/<name>` plugin package (ui-workspace is a complete example; ui-sidebar/ui-question are minimal skeletons):
Bringing up a new `packages/client/<name>` plugin package (ui-workspace is a complete example; ui-sidebar/ui-user-questions are minimal skeletons):
1. **Package skeleton**: `package.json` (`@deepseek-ai/dsh-client-<name>`, exports `.`/`./invariant`/`./client`/`./src/*`/`./package.json`, `dsh.client` manifest, `files` list), `tsconfig.json` (extends `tsconfig.base.client.json`, one `references` entry per workspace dependency plus `support/invariants`), `tsdown.config.ts` (`clientBundle(id, ['lib/types/index.js', 'lib/types/invariant.js'])`), `src/index.ts` (empty node-half apply), `src/invariant.ts` (companion with a real reason), `src/css-modules.d.ts` when using CSS Modules, `README.md` with the Model Experience section.
1. **Package skeleton**: `package.json` (`@deepseek-ai/dsh-client-<name>`, exports `.`/`./invariant`/`./client`/`./src/*`/`./package.json`, `dsh.client` manifest, `files` list), `tsconfig.json` (extends `tsconfig.base.client.json`, one `references` entry per workspace dependency plus `runtime-diagnostics/invariants`), `tsdown.config.ts` (`clientBundle(id, ['lib/types/index.js', 'lib/types/invariant.js'])`), `src/index.ts` (empty node-half apply), `src/invariant.ts` (companion with a real reason), `src/css-modules.d.ts` when using CSS Modules, `README.md` with the Model Experience section.
2. **Three registration surfaces, all required** (missing any one fails at a different, later point): the `tsconfig.client.json` aggregate `references` entry; a `dsh.client` row in `packages/bundle/web-app/cordis.patch.yml`; a `packages/bundle/web-app/package.json` dependency (profile boots resolve bare row names through the healed `$DSH_HOME/profiles/node_modules` fallback, which mirrors the app's and each bundle's declared dependencies — a row whose package no manifest declares fails to import). `pnpm-workspace.yaml` already globs `packages/*/*`.
3. **dsh.client manifest semantics**: `platform: 'web'` always; `immediately: true` only for stage-one-prefetch infrastructure rows. `inject` lists package-name dependency edges — they are **informational only** (preflight display, HMR diffing); they do not sequence entry activation or apply order. Activation order is cordis fiber inject waiting on *services*, nothing else.
4. **Registering into another package's slot**: apply order is unconstrained, and a business service is not a declaration barrier. Use `ctx.slots.inject(name, () => ctx.slots.register(...))`; it waits on the actual declaration, removes the contribution when that declaration collapses, reruns after redeclaration, and leaves with the caller's plugin fiber. Return a generator yielding each registration when several contributions must install and roll back atomically. A bare `slots.register` into an undeclared slot remains an error; keep service edges only for services the contribution actually reads.

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/client/README.md
README.md: b9452d1f763be5be6953cb7973da8a2c909ed979
README.zh.md: 0dfb6e6b6d619f111e36d5bdf57d127ac1ca9ef5
README.md: f22ae99113f7f313c60304ac4c5fc53c8e3172ab
README.zh.md: c4aa20c9f5f317bbe5dbb6f6731f6eb6d5d7ac7c

View File

@@ -14,7 +14,7 @@ The browser side of the dsh web GUI: shell boot, browser-host communication, sha
| [`hmr/`](hmr/README.md) | Refreshes client plugins during development. |
| [`locale/`](locale/README.md) | Provides localization preferences and message dictionaries. |
| [`schema-form/`](schema-form/README.md) | Provides schema-backed draft handling for settings editors. |
| [`test-runtime/`](test-runtime/README.md) | Provides shared repository test support for client feature packages. |
| [`test-runtime/`](../test-support/client-runtime/README.md) | Provides shared repository test support for client feature packages. |
| [`ui-slots/`](ui-slots/README.md) | Defines how UI features register and compose extension slots. |
| [`ui-theme/`](ui-theme/README.md) | Applies the selected color theme. |
| [`ui-primitives/`](ui-primitives/README.md) | Provides shared React controls, icons, and content renderers. |
@@ -27,21 +27,21 @@ The browser side of the dsh web GUI: shell boot, browser-host communication, sha
| [`ui-workflow-run/`](ui-workflow-run/README.md) | Replays durable workflow runs as nested Chat disclosures with live-only child navigation. |
| [`ui-goal/`](ui-goal/README.md) | Presents and manages the current goal. |
| [`ui-trajectory/`](ui-trajectory/README.md) | Presents alternate views of agent activity. |
| [`ui-command/`](ui-command/README.md) | Provides session-aware command discovery and dispatch. |
| [`ui-slash/`](ui-slash/README.md) | Coordinates inline command and reference suggestions. |
| [`ui-commands/`](ui-commands/README.md) | Provides session-aware command discovery and dispatch. |
| [`ui-input-trigger/`](ui-input-trigger/README.md) | Coordinates inline command and reference suggestions. |
| [`ui-skill/`](ui-skill/README.md) | Adds skill references to inline suggestions. |
| [`ui-subagent/`](ui-subagent/README.md) | Provides subagent navigation, child transcript states, and inline references. |
| [`ui-task/`](ui-task/README.md) | Lists this session's background tasks in the conversation header. |
| [`ui-model/`](ui-model/README.md) | Provides model selection in conversation surfaces. |
| [`ui-permission/`](ui-permission/README.md) | Configures default permissions and switches the current session's access. |
| [`ui-jobs/`](ui-jobs/README.md) | Lists this session's background jobs in the conversation header. |
| [`ui-model-selection/`](ui-model-selection/README.md) | Provides model selection in conversation surfaces. |
| [`ui-permission/`](ui-permission-presets/README.md) | Configures default permissions and switches the current session's access. |
| [`ui-plan/`](ui-plan/README.md) | Presents active plan-mode status and its exit control. |
| [`ui-plugin-config/`](ui-plugin-config/README.md) | Owns the Plugins settings section, its tab extension point, and configurable host-plane plugin cards. |
| [`ui-question/`](ui-question/README.md) | Presents interactive questions requested by the agent. |
| [`ui-settings-plugins/`](ui-settings-plugins/README.md) | Owns the Plugins settings section, its tab extension point, and configurable host-plane plugin cards. |
| [`ui-user-questions/`](ui-user-questions/README.md) | Presents interactive questions requested by the agent. |
| [`ui-agent-preset/`](ui-agent-preset/README.md) | Selects a session's agent preset and authors preset compositions. |
| [`ui-settings/`](ui-settings/README.md) | Hosts the settings interface and its extension areas. |
| [`ui-settings-general/`](ui-settings-general/README.md) | Provides the general settings section. |
| [`ui-models/`](ui-models/README.md) | Provides model-provider configuration and DeepSeek onboarding. |
| [`ui-plugins/`](ui-plugins/README.md) | Contributes the read-only Host Loader inventory tab to Plugins settings. |
| [`ui-settings-models/`](ui-settings-models/README.md) | Provides model-provider configuration and DeepSeek onboarding. |
| [`ui-settings-plugin-inventory/`](ui-settings-plugin-inventory/README.md) | Contributes the read-only Host Loader inventory tab to Plugins settings. |
Each child reference owns its contract and detailed behavior. The [slot system standard](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md) and [web client architecture note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) own the cross-package composition and loading decisions.

View File

@@ -14,7 +14,7 @@ dsh web GUI 的浏览器侧shell 启动、浏览器与宿主通信、共享 U
| [`hmr/`](hmr/README.md) | 在开发期间刷新客户端插件。 |
| [`locale/`](locale/README.md) | 提供本地化偏好与消息词典。 |
| [`schema-form/`](schema-form/README.md) | 为设置编辑器提供 schema 驱动的草稿处理。 |
| [`test-runtime/`](test-runtime/README.md) | 为客户端功能包提供共享的仓库测试支持。 |
| [`test-runtime/`](../test-support/client-runtime/README.md) | 为客户端功能包提供共享的仓库测试支持。 |
| [`ui-slots/`](ui-slots/README.md) | 定义 UI 功能注册和组合扩展 slot 的方式。 |
| [`ui-theme/`](ui-theme/README.md) | 应用所选颜色主题。 |
| [`ui-primitives/`](ui-primitives/README.md) | 提供共享 React 控件、图标和内容渲染器。 |
@@ -27,21 +27,21 @@ dsh web GUI 的浏览器侧shell 启动、浏览器与宿主通信、共享 U
| [`ui-workflow-run/`](ui-workflow-run/README.md) | 把持久工作流运行回放为 Chat 嵌套折叠项,并只为实时子 Session 提供导航。 |
| [`ui-goal/`](ui-goal/README.md) | 展示和管理当前目标。 |
| [`ui-trajectory/`](ui-trajectory/README.md) | 提供 agent智能体活动的其他视图。 |
| [`ui-command/`](ui-command/README.md) | 提供会话感知的命令发现与分发。 |
| [`ui-slash/`](ui-slash/README.md) | 协调内联命令和引用建议。 |
| [`ui-commands/`](ui-commands/README.md) | 提供会话感知的命令发现与分发。 |
| [`ui-input-trigger/`](ui-input-trigger/README.md) | 协调内联命令和引用建议。 |
| [`ui-skill/`](ui-skill/README.md) | 向内联建议添加 skill技能引用。 |
| [`ui-subagent/`](ui-subagent/README.md) | 提供 subagent子 agent导航、子级 transcript文本记录的状态和内联引用。 |
| [`ui-task/`](ui-task/README.md) | 在会话标题栏列出当前会话的后台任务。 |
| [`ui-model/`](ui-model/README.md) | 在对话界面中提供模型选择。 |
| [`ui-permission/`](ui-permission/README.md) | 配置默认权限并切换当前会话的访问模式。 |
| [`ui-jobs/`](ui-jobs/README.md) | 在会话标题栏列出当前会话的后台任务。 |
| [`ui-model-selection/`](ui-model-selection/README.md) | 在对话界面中提供模型选择。 |
| [`ui-permission/`](ui-permission-presets/README.md) | 配置默认权限并切换当前会话的访问模式。 |
| [`ui-plan/`](ui-plan/README.md) | 展示生效中的 plan mode 状态及其退出控件。 |
| [`ui-plugin-config/`](ui-plugin-config/README.md) | 拥有“插件”设置分区、它的标签页扩展点,以及可配置的宿主平面插件卡片。 |
| [`ui-question/`](ui-question/README.md) | 展示 agent 请求的交互式问题。 |
| [`ui-settings-plugins/`](ui-settings-plugins/README.md) | 拥有“插件”设置分区、它的标签页扩展点,以及可配置的宿主平面插件卡片。 |
| [`ui-user-questions/`](ui-user-questions/README.md) | 展示 agent 请求的交互式问题。 |
| [`ui-agent-preset/`](ui-agent-preset/README.md) | 选择会话的 agent 预设,并编写预设组合。 |
| [`ui-settings/`](ui-settings/README.md) | 承载设置界面及其扩展区域。 |
| [`ui-settings-general/`](ui-settings-general/README.md) | 提供常规设置分区。 |
| [`ui-models/`](ui-models/README.md) | 提供模型提供方配置与 DeepSeek 配置引导。 |
| [`ui-plugins/`](ui-plugins/README.md) | 向“插件”设置贡献只读的 Host Loader 清单标签页。 |
| [`ui-settings-models/`](ui-settings-models/README.md) | 提供模型提供方配置与 DeepSeek 配置引导。 |
| [`ui-settings-plugin-inventory/`](ui-settings-plugin-inventory/README.md) | 向“插件”设置贡献只读的 Host Loader 清单标签页。 |
每个子文档负责自身的约定和详细行为。[slot 系统标准](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md)与 [Web 客户端架构 Agent Note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.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/client/connection/README.md
README.md: d3727df981ecbc022345def48b38fbc879e29cb2
README.zh.md: 5e632bf6d7135bda60178f0699691aff2d5623db
README.md: 3315552dd0400697d5a96639598c5f5bb2bb4f5b
README.zh.md: 0dc251f1e5410bd0d74ef450c6d8df3dc9cdab81

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + current-page loopback state + observable generation-scoped `hostDescription` + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` abstraction, and the loop's sink/config types. Each successful readiness handshake publishes the exact `host.describe` value before `onConnected`; generation loss and explicit stop clear it, so native-capability consumers never retain a disconnected answer. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for `events.mux` and `events.host`; the in-process carrier satisfies the same two-stream abstraction. The Host half owns the single `/api` route and its Fetch bridge; a registered TypeRT interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the `/api` Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived `ctx.connection.isLoopback` state. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`openDocument`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from — and the agent-preset authoring plane, `agentPreset.read`/`copy`/`openDocument`/`remove`, since a composition names the plugins a session runs, so reading one is reconnaissance, and copy/remove/openDocument manage the roster and drive the host desktop (authoring is copy-only, so none of them accepts composition text or a path); `agentPreset.list` and `agentPreset.select` stay out — the roster carries only ids and trust, and choosing a preset grants nothing `session.create`'s own `agentPreset` did not, over a default that already carries bash) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the [WebSocket downlink carrier Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md).
Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + current-page loopback state + observable generation-scoped `hostDescription` + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` abstraction, and the loop's sink/config types. Each successful readiness handshake publishes the exact `host.describe` value before `onConnected`; generation loss and explicit stop clear it, so native-capability consumers never retain a disconnected answer. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for `events.mux` and `events.host`; the in-process carrier satisfies the same two-stream abstraction. The Host half owns the single `/api` route and its Fetch bridge; a registered Typert interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the `/api` Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived `ctx.connection.isLoopback` state. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`openDocument`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from — and the agent-preset authoring plane, `agentPreset.read`/`copy`/`openDocument`/`remove`, since a composition names the plugins a session runs, so reading one is reconnaissance, and copy/remove/openDocument manage the roster and drive the host desktop (authoring is copy-only, so none of them accepts composition text or a path); `agentPreset.list` and `agentPreset.select` stay out — the roster carries only ids and trust, and choosing a preset grants nothing `session.create`'s own `agentPreset` did not, over a default that already carries bash) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the [WebSocket downlink carrier Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md).
## /api browser-trust fence

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 当前页面的 loopback 状态 + 可观察且按 generation 生效的 `hostDescription` + 单消费方流循环启动器);导出表层携带协议约定类型、`AbstractApiClient` 抽象,以及循环的 sink配置类型。每次就绪握手成功后都会在 `onConnected` 之前发布完整的 `host.describe`generation 失效或显式 stop 会清空它,因此原生能力消费者不会保留已经断线的判断。浏览器载体以 HTTP POST 发送 unaryrespond并为 `events.mux``events.host` 各开一条只下行的 WebSocket进程内载体满足同一双流抽象。Host half 持有唯一 `/api` route 及其 Fetch bridge已注册的 TypeRT interceptor 会先认领自己的 Remote endpoint未认领请求再回退 API Proxy。Loopback hostname 判定逻辑留在包内部:`/api` Host fence 与 WebSocket upgrade 会直接使用它,其他客户端插件则消费派生的 `ctx.connection.isLoopback` 状态。node 半侧的 `/api` 路由让特权方法集(`host.pickDirectory``host.openPath`,以及整个配置面——`settings.describe`/`openDocument`/`update`/`replace`/`mutate``credentials.describe`/`set`/`unset`;读取与原生操作也在内,因为 describe 会返回已暴露的配置、打开操作会作用于 Host 桌面,而探测任意引用会报出某条凭据来自何处——以及 agent智能体 preset 的创作面 `agentPreset.read`/`copy`/`openDocument`/`remove`,因为组装指明了一个会话所运行的插件,读取它是侦察,而 copy/remove/openDocument 管理名单并驱动宿主桌面(创作只有复制一种写入,因此这些方法都不接收组装文本或路径);`agentPreset.list``agentPreset.select` 不在其中——名单只携带 id 与信任级别,而选择一个 preset 并不比 `session.create` 自带的 `agentPreset` 多给任何能力,何况默认 preset 本就带着 bash以空信任表过信任 fence从而钉在回环——已声明的 `trustedHosts` 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台载体与 ConnectionController 循环属于包内部apply 负责选择并驱动它们。下行边界见 [WebSocket 下行载体 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md)。
协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 当前页面的 loopback 状态 + 可观察且按 generation 生效的 `hostDescription` + 单消费方流循环启动器);导出表层携带协议约定类型、`AbstractApiClient` 抽象,以及循环的 sink配置类型。每次就绪握手成功后都会在 `onConnected` 之前发布完整的 `host.describe`generation 失效或显式 stop 会清空它,因此原生能力消费者不会保留已经断线的判断。浏览器载体以 HTTP POST 发送 unaryrespond并为 `events.mux``events.host` 各开一条只下行的 WebSocket进程内载体满足同一双流抽象。Host half 持有唯一 `/api` route 及其 Fetch bridge已注册的 Typert interceptor 会先认领自己的 Remote endpoint未认领请求再回退 API Proxy。Loopback hostname 判定逻辑留在包内部:`/api` Host fence 与 WebSocket upgrade 会直接使用它,其他客户端插件则消费派生的 `ctx.connection.isLoopback` 状态。node 半侧的 `/api` 路由让特权方法集(`host.pickDirectory``host.openPath`,以及整个配置面——`settings.describe`/`openDocument`/`update`/`replace`/`mutate``credentials.describe`/`set`/`unset`;读取与原生操作也在内,因为 describe 会返回已暴露的配置、打开操作会作用于 Host 桌面,而探测任意引用会报出某条凭据来自何处——以及 agent智能体 preset 的创作面 `agentPreset.read`/`copy`/`openDocument`/`remove`,因为组装指明了一个会话所运行的插件,读取它是侦察,而 copy/remove/openDocument 管理名单并驱动宿主桌面(创作只有复制一种写入,因此这些方法都不接收组装文本或路径);`agentPreset.list``agentPreset.select` 不在其中——名单只携带 id 与信任级别,而选择一个 preset 并不比 `session.create` 自带的 `agentPreset` 多给任何能力,何况默认 preset 本就带着 bash以空信任表过信任 fence从而钉在回环——已声明的 `trustedHosts` 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台载体与 ConnectionController 循环属于包内部apply 负责选择并驱动它们。下行边界见 [WebSocket 下行载体 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md)。
## /api 浏览器信任栅栏

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-connection",
"description": "Wire consumer layer: HTTP-up/WebSocket-down client, ConnectionController dual streams with reconnect, and fixture api",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},

View File

@@ -17,7 +17,7 @@ export type {
SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi,
SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt,
TaskView,
JobView,
} from '@deepseek-ai/dsh-host-apiproxy/api'
export type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation'
export type {

View File

@@ -108,7 +108,7 @@ function sgr(code: number, body: string): string {
* TERMINAL_EXIT_STATUS and deliberately absent from this text: the real bash
* presenter CONSUMES its `[exit code: N]` marker out of the body, because a
* terminal card shows the exit as its own pill and leaving the marker in would
* render it twice (packages/bash/tool-bash/src/render.ts).
* render it twice (packages/shell/tool-bash/src/render.ts).
*/
const TERMINAL_OUTPUT_FIXTURE = [
sgr(1, 'Running 4 checks'),
@@ -771,7 +771,7 @@ function planViewOf(log: readonly SessionEvent[]): { active: boolean; pending: b
}
/** Fixture parallel of the host's projection units: whole current values per key over the full log. */
/** Fixture preset table (the host PermissionService defaults). */
/** Fixture preset table (the host PermissionPresetService defaults). */
const PERMISSION_PRESETS: Record<string, { sandbox: string; approval: string; description: string }> = {
'workspace-write': { sandbox: 'workspace-write', approval: 'ask', description: 'Write inside the workspace and permitted temporary directories; wider retries require approval.' },
'danger-full-access': { sandbox: 'danger-full-access', approval: 'never', description: 'Full file access without approval prompts.' },

View File

@@ -22,7 +22,7 @@ export type {
ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
MessageId, ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels,
SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt,
TaskView,
JobView,
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
HostDescription, IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,

View File

@@ -2,7 +2,7 @@
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-attachment'
// Activates the httpServer Context merge used below.
// Activates the webServer Context merge used below.
import type { WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'
import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy'
import { API_PATH, HOST_EVENTS_PATH, MUX_EVENTS_PATH } from './api-path.ts'
@@ -44,7 +44,7 @@ function assertImageBodyCapacity(ctx: Context, maxRequestBodyBytes: number): voi
}
/** Services required before providing Connection; API Proxy is an optional `/api` fallback. */
export const inject = ['httpServer']
export const inject = ['webServer']
/** Plugin config: the deployment's non-loopback serving authorities. */
export interface ConnectionConfig {
@@ -170,7 +170,7 @@ export function apply(ctx: Context, config?: ConnectionConfig): void {
await bridge(req, res, fetchHandler, maxRequestBodyBytes)
},
}
ctx.effect(() => ctx.httpServer.register(route), 'client-connection: /api route')
ctx.effect(() => ctx.webServer.register(route), 'client-connection: /api route')
ctx.inject(['apiProxy'], (apiCtx) => {
assertImageBodyCapacity(apiCtx, maxRequestBodyBytes)
const downlinks = new WebSocketDownlinks(apiCtx.apiProxy)
@@ -178,7 +178,7 @@ export function apply(ctx: Context, config?: ConnectionConfig): void {
path: string,
handle: WebUpgradeRoute['handler'],
): void => {
apiCtx.effect(() => apiCtx.httpServer.registerUpgrade({
apiCtx.effect(() => apiCtx.webServer.registerUpgrade({
path,
handler: (req, socket, head) => {
if (!isTrustedApiRequest(req, trustedHosts)) {

View File

@@ -109,7 +109,7 @@ export class HostConnectionService extends Service implements HostConnectionHand
},
}
return owner.effect(
() => owner.httpServer.register(route),
() => owner.webServer.register(route),
`client-connection: ${channel} rpc channel`,
)
}

View File

@@ -9,14 +9,14 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy/api'
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment'
import { RpcId, type ClientRequest } from '@deepseek-ai/dsh-host-apiproxy/api'
import type { HttpServerService, WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'
import type { WebServer, WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'
import { API_PATH, apply, HOST_EVENTS_PATH, inject, MUX_EVENTS_PATH, type HostConnectionHandle } from '../src/index.ts'
/** Structural httpServer fake recording both route registries. */
/** Structural webServer fake recording both route registries. */
function fakeHttpServer(
routes: WebRoute[],
upgrades: WebUpgradeRoute[],
): Pick<HttpServerService, 'register' | 'registerUpgrade' | 'tapIndex' | 'port'> {
): Pick<WebServer, 'register' | 'registerUpgrade' | 'tapIndex' | 'port'> {
return {
register(route) {
if (routes.some(candidate => candidate.kind === route.kind && candidate.path === route.path)) {
@@ -82,7 +82,7 @@ async function mounted(config?: { trustedHosts?: string[] }): Promise<{
const ctx = new Context()
const routes: WebRoute[] = []
const upgrades: WebUpgradeRoute[] = []
ctx.provide('httpServer', fakeHttpServer(routes, upgrades) as HttpServerService)
ctx.provide('webServer', fakeHttpServer(routes, upgrades) as WebServer)
ctx.provide('apiProxy', {} as unknown as ApiProxy)
const fiber = ctx.plugin({ inject: [...inject], apply }, config)
await fiber.await()
@@ -93,7 +93,7 @@ describe('connection node half', () => {
it('fails loud when the carrier cap cannot hold the configured image batch', () => {
const ctx = new Context()
const routes: WebRoute[] = []
ctx.provide('httpServer', fakeHttpServer(routes, []) as HttpServerService)
ctx.provide('webServer', fakeHttpServer(routes, []) as WebServer)
ctx.provide('attachments', {
imageLimits: { maxMessageImageBytes: 20 * 1024 * 1024 },
} as AttachmentStore)
@@ -107,7 +107,7 @@ describe('connection node half', () => {
const routes: WebRoute[] = []
const upgrades: WebUpgradeRoute[] = []
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer(routes, upgrades) as HttpServerService)
ctx.provide('webServer', fakeHttpServer(routes, upgrades) as WebServer)
ctx.provide('apiProxy', {} as unknown as ApiProxy)
const fiber = ctx.plugin({ inject: [...inject], apply }, { trustedHosts: ['harness.internal/path'] })
await expect(fiber).rejects.toThrow(/not a bare host\[:port\] authority/)
@@ -216,7 +216,7 @@ describe('connection node half', () => {
it('provides a disposable dedicated RPC channel without requiring apiProxy', async () => {
const ctx = new Context()
const routes: WebRoute[] = []
ctx.provide('httpServer', fakeHttpServer(routes, []) as HttpServerService)
ctx.provide('webServer', fakeHttpServer(routes, []) as WebServer)
const fiber = ctx.plugin({ inject: [...inject], apply })
await fiber.await()
expect(routes).toHaveLength(1)
@@ -262,7 +262,7 @@ describe('connection node half', () => {
it('dispatches claimed /api endpoints before the API Proxy fallback and withdraws the claim', async () => {
const ctx = new Context()
const routes: WebRoute[] = []
ctx.provide('httpServer', fakeHttpServer(routes, []) as HttpServerService)
ctx.provide('webServer', fakeHttpServer(routes, []) as WebServer)
ctx.provide('apiProxy', {} as unknown as ApiProxy)
const fiber = ctx.plugin({ inject: [...inject], apply }, { trustedHosts: ['harness.example'] })
await fiber.await()
@@ -340,7 +340,7 @@ describe('connection node half', () => {
it('applies the configured trust fence and JSON envelope checks to generic channels', async () => {
const ctx = new Context()
const routes: WebRoute[] = []
ctx.provide('httpServer', fakeHttpServer(routes, []) as HttpServerService)
ctx.provide('webServer', fakeHttpServer(routes, []) as WebServer)
const fiber = ctx.plugin({ inject: [...inject], apply }, { trustedHosts: ['harness.example'] })
await fiber.await()
const connection = ctx.get('connection') as HostConnectionHandle

View File

@@ -40,7 +40,7 @@
"path": "../../llm/llm"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../util/brand"

View File

@@ -27,7 +27,7 @@
"path": "../../host/webserver"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-hmr",
"description": "Dev-only hot-reload driver for script-loaded client entries: SSE rebuilt frames → invalidate/prefetch → fiber swap through the vendored Loader entry",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},

View File

@@ -12,7 +12,7 @@ import { statSync } from 'node:fs'
import type { ServerResponse } from 'node:http'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
// Empty type imports carry the clientModuleHost/httpServer Context merges.
// Empty type imports carry the clientModuleHost/webServer Context merges.
import type {} from '@deepseek-ai/dsh-client-modules'
import type {} from '@deepseek-ai/dsh-host-webserver'
import type { PluginsEventFrame } from './events.ts'
@@ -25,7 +25,7 @@ export { EVENTS_ENDPOINT } from './events.ts'
export const name = 'client-hmr'
/** Required services: the web plugin table and the route registry. */
export const inject = ['clientModuleHost', 'httpServer']
export const inject = ['clientModules', 'webServer']
/** Plugin config, validated by the same-named schemastery schema. */
export interface Config {
@@ -51,7 +51,7 @@ interface WatchedBundle {
/**
* Mount the dev chain: bundle watches, rebuilt reporting, and the SSE channel.
* @param ctx - host plugin context carrying clientModuleHost and httpServer.
* @param ctx - host plugin context carrying clientModuleHost and webServer.
* @param config - validated {@link Config}.
*/
export function apply(ctx: Context, config: Config): void {
@@ -65,7 +65,7 @@ export function apply(ctx: Context, config: Config): void {
try {
// rebuilt() re-hashes; an unchanged hash stays silent (clientModuleHost
// fires onRebuilt only on a real rev change).
ctx.clientModuleHost.rebuilt(id)
ctx.clientModules.rebuilt(id)
} catch (error) {
const code = (error as NodeJS.ErrnoException).code
if (code === 'ENOENT') {
@@ -117,8 +117,8 @@ export function apply(ctx: Context, config: Config): void {
// rows (or rows whose bundle path moved), add watches for new rows.
const syncWatches = (): void => {
const rows = new Map<string, string>()
for (const row of ctx.clientModuleHost.graph().entries) {
const path = ctx.clientModuleHost.clientPath(row.id)
for (const row of ctx.clientModules.graph().entries) {
const path = ctx.clientModules.clientPath(row.id)
if (path !== undefined) rows.set(row.id, path)
}
for (const [id, watch] of watched) {
@@ -135,7 +135,7 @@ export function apply(ctx: Context, config: Config): void {
// rows arriving later (boot-window activations, including this plugin's
// own row — no self-exemption, a modules/hmr rebuild rides the same chain).
syncWatches()
const unsubscribe = ctx.clientModuleHost.onGraphChanged(syncWatches)
const unsubscribe = ctx.clientModules.onGraphChanged(syncWatches)
const timer = setInterval(pollWatches, pollIntervalMs)
timer.unref()
return () => {
@@ -157,13 +157,13 @@ export function apply(ctx: Context, config: Config): void {
// Comment line on open so clients/proxies see a live channel even when
// no rebuild ever happens; EventSource frame parsing skips it naturally.
res.write(': connected\n\n')
res.write(sseData({ type: 'graph', graph: ctx.clientModuleHost.graph() }))
res.write(sseData({ type: 'graph', graph: ctx.clientModules.graph() }))
connections.add(res)
res.on('close', () => { connections.delete(res) })
}
ctx.effect(() => {
const disposeRoute = ctx.httpServer.register({
const disposeRoute = ctx.webServer.register({
kind: 'exact',
path: EVENTS_ENDPOINT,
handler: (req, res) => {
@@ -177,7 +177,7 @@ export function apply(ctx: Context, config: Config): void {
connect(res)
},
})
const unsubscribe = ctx.clientModuleHost.onRebuilt((id, rev) => {
const unsubscribe = ctx.clientModules.onRebuilt((id, rev) => {
const line = sseData({ type: 'rebuilt', id, rev })
for (const res of connections) res.write(line)
})

View File

@@ -7,8 +7,8 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { WebBootGraph, ClientModuleHostService } from '@deepseek-ai/dsh-client-modules'
import type { WebRoute, HttpServerService } from '@deepseek-ai/dsh-host-webserver'
import type { WebBootGraph, ClientModuleRegistry } from '@deepseek-ai/dsh-client-modules'
import type { WebRoute, WebServer } from '@deepseek-ai/dsh-host-webserver'
import { apply, Config, EVENTS_ENDPOINT, inject } from '../src/index.ts'
const POLL_MS = 20
@@ -23,7 +23,7 @@ afterEach(() => { rmSync(dir, { recursive: true, force: true }) })
* Structural (Pick+cast): the plugin only touches the read/notify surface;
* the service class carries private scan state a literal need not reproduce.
*/
type FakeHost = ClientModuleHostService & { rebuiltCalls: string[]; fireGraphChanged(): void }
type FakeHost = ClientModuleRegistry & { rebuiltCalls: string[]; fireGraphChanged(): void }
interface FakeHostOptions {
beforeGraphRead?: () => void
rebuilt?: (id: string) => string | undefined
@@ -58,8 +58,8 @@ function fakeClientModuleHost(rows: Map<string, string>, options: FakeHostOption
// Structural fake: the plugin only touches register(); the service class
// carries private state a literal cannot (and need not) reproduce.
function fakeHttpServer(routes: WebRoute[]): HttpServerService {
const fake: Pick<HttpServerService, 'register' | 'tapIndex' | 'port'> = {
function fakeHttpServer(routes: WebRoute[]): WebServer {
const fake: Pick<WebServer, 'register' | 'tapIndex' | 'port'> = {
register(route) {
routes.push(route)
return () => { routes.splice(routes.indexOf(route), 1) }
@@ -67,13 +67,13 @@ function fakeHttpServer(routes: WebRoute[]): HttpServerService {
tapIndex: () => () => {},
port: 0,
}
return fake as HttpServerService
return fake as WebServer
}
async function mount(clientModuleHost: FakeHost, httpServer: HttpServerService) {
async function mount(clientModuleHost: FakeHost, webServer: WebServer) {
const ctx = new Context()
ctx.provide('clientModuleHost', clientModuleHost)
ctx.provide('httpServer', httpServer)
ctx.provide('clientModules', clientModuleHost)
ctx.provide('webServer', webServer)
const fiber = ctx.plugin(
{ inject: [...inject], Config, apply },
{ pollIntervalMs: POLL_MS },

View File

@@ -30,7 +30,7 @@
"path": "../../../vendor/schemastery"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

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/client/locale/README.md
README.md: 5bea46cd4e3ace61bd2251610abdf0812ded9604
README.zh.md: 2333bc7c2b2f5918c35286064c50131153ee8711
README.md: a63807f093dc12831a41196e61008151868e3205
README.zh.md: b302e6055ba30d2db0948c0a6f1551c4a9dcb24c

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
Locale plugin: LocaleService — the `zh`/`en` preference stored as `locale.preference` in `$DSH_HOME/settings.yaml`; when that explicit Host value is absent, a fresh browser starts provisionally in the language `navigator` asks for (primary-subtag matching, with `zh` when it asks for no language this app ships). The Host read runs after plugin activation so an unavailable settings service cannot block the page; its result replaces the provisional browser value live. Remote browsers retain only a process-local selection because the settings API is loopback-only. `locale/change` fires on switches. The service also owns the ns×locale dictionary registry (typed `register(ns, {zh, en})` checked against `LocaleNamespaceMap`, `bind(ns)``TranslateNS<ns>`; lookup chain ns → common → zh → key), implements the slot system's `LocaleFace`, and installs itself through `ctx.slots.installLocale`, backing the framework-injected `t` standard seat (`Translate`/`TranslateNS` are ui-slots types; import them from there — this package only re-exports for dictionary owners' convenience). The [Host-backed preferences decision](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md) owns the persistence boundary.
Locale plugin: LocaleRuntime — the `zh`/`en` preference stored as `locale.preference` in `$DSH_HOME/settings.yaml`; when that explicit Host value is absent, a fresh browser starts provisionally in the language `navigator` asks for (primary-subtag matching, with `zh` when it asks for no language this app ships). The Host read runs after plugin activation so an unavailable settings service cannot block the page; its result replaces the provisional browser value live. Remote browsers retain only a process-local selection because the settings API is loopback-only. `locale/change` fires on switches. The service also owns the ns×locale dictionary registry (typed `register(ns, {zh, en})` checked against `LocaleNamespaceMap`, `bind(ns)``TranslateNS<ns>`; lookup chain ns → common → zh → key), implements the slot system's `LocaleFace`, and installs itself through `ctx.slots.installLocale`, backing the framework-injected `t` standard seat (`Translate`/`TranslateNS` are ui-slots types; import them from there — this package only re-exports for dictionary owners' convenience). The [Host-backed preferences decision](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md) owns the persistence boundary.
## Model Experience

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
locale 插件LocaleService——`zh``en` 偏好以 `locale.preference` 存储在 `$DSH_HOME/settings.yaml` 中;若没有显式 Host 值,全新浏览器会暂时使用 `navigator` 请求的语言(按主子标签匹配;若其请求的语言本应用都不提供,则使用 `zh`。Host 读取在插件激活后执行,因此 settings 服务不可用不会阻塞页面读取结果会实时替换浏览器暂定值。settings API 仅限回环请求,因此远程浏览器的选择仅保留在进程内。`locale/change` 仅在切换语言时触发。该服务还拥有 ns×locale 字典注册表(类型化 `register(ns, {zh, en})``LocaleNamespaceMap` 校验,`bind(ns)``TranslateNS<ns>`;查找链 ns → common → zh → key实现 slot 系统的 `LocaleFace`,并经 `ctx.slots.installLocale` 自行安装,支撑框架注入的 `t` 标准席位(`Translate``TranslateNS` 是 ui-slots 的类型;请从那里导入——本包的再导出仅为字典所有者提供便利)。该持久化边界由[Host settings 支撑的偏好决策](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md)拥有。
locale 插件LocaleRuntime——`zh``en` 偏好以 `locale.preference` 存储在 `$DSH_HOME/settings.yaml` 中;若没有显式 Host 值,全新浏览器会暂时使用 `navigator` 请求的语言(按主子标签匹配;若其请求的语言本应用都不提供,则使用 `zh`。Host 读取在插件激活后执行,因此 settings 服务不可用不会阻塞页面读取结果会实时替换浏览器暂定值。settings API 仅限回环请求,因此远程浏览器的选择仅保留在进程内。`locale/change` 仅在切换语言时触发。该服务还拥有 ns×locale 字典注册表(类型化 `register(ns, {zh, en})``LocaleNamespaceMap` 校验,`bind(ns)``TranslateNS<ns>`;查找链 ns → common → zh → key实现 slot 系统的 `LocaleFace`,并经 `ctx.slots.installLocale` 自行安装,支撑框架注入的 `t` 标准席位(`Translate``TranslateNS` 是 ui-slots 的类型;请从那里导入——本包的再导出仅为字典所有者提供便利)。该持久化边界由[Host settings 支撑的偏好决策](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md)拥有。
## 模型体验

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-locale",
"description": "Locale plugin: Host-backed zh/en preference, browser-derived fallback, locale snapshots, and typed namespace dictionaries",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},

View File

@@ -71,7 +71,7 @@ export interface LocaleSnapshot {
declare module '@deepseek-ai/cordis' {
interface Context {
locale: LocaleService
locale: LocaleRuntime
}
interface Events {
/**
@@ -111,7 +111,7 @@ const LOCALES: readonly LocaleDefinition[] = Object.freeze([
* LocaleFace getSnapshot/subscribe pair the render machinery consumes
* (installed via `ctx.slots.installLocale`).
*/
export class LocaleService {
export class LocaleRuntime {
private dicts = new Map<string, Map<string, LocaleDict>>()
private bound = new Map<string, Translate>()
private snapshot: LocaleSnapshot
@@ -354,7 +354,7 @@ export const inject = ['slots', 'connection', 'remote', 'settingsScope']
*/
export function apply(ctx: ClientContext): void {
const host = ctx.settingsScope.bind<LocaleSettings>({ namespace: LOCALE_SETTINGS_NAMESPACE })
const locale = new LocaleService(ctx, host)
const locale = new LocaleRuntime(ctx, host)
locale.register(COMMON_NS, { zh, en })
locale.register(SETTINGS_NS, { zh: settingsZh, en: settingsEn })
ctx.provide('locale', locale)

View File

@@ -3,13 +3,13 @@
* recovery after an HMR collapse of the declaring entry. */
import { Context } from '@deepseek-ai/cordis'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import { SettingsScopeService } from '@deepseek-ai/dsh-client-ui-settings/client'
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import { SettingsScopeBinder } from '@deepseek-ai/dsh-client-ui-settings/client'
import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
import {
apply, inject, SETTINGS_NS,
} from '@deepseek-ai/dsh-client-locale/client'
import type { LanguageRowInjected, LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import type { LanguageRowInjected, LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { LOCALE_SETTINGS_NAMESPACE, LocaleSettingsSchema } from '../src/locale-settings.ts'
import { LanguageRow } from '../src/client/LanguageRow.tsx'
import type { createLanguageRowStore } from '../src/client/settings-store.ts'
@@ -18,7 +18,7 @@ const SLOT = 'settings.general.item'
async function bench() {
const ctx = new Context()
await ctx.plugin(SlotsService).await()
await ctx.plugin(SlotRegistry).await()
let preference: string | undefined
let revision = 0
const namespace = () => ({
@@ -47,15 +47,15 @@ async function bench() {
ctx.provide('connection', { api: { settings: { describe, mutate } }, isLoopback: true } as never)
// The settings transport and the forwarded-event port the plugin injects.
new TestRemote(ctx)
await ctx.plugin(SettingsScopeService).await()
await ctx.plugin(SettingsScopeBinder).await()
return {
ctx, slots: ctx.get('slots') as SlotsService, describe, mutate,
ctx, slots: ctx.get('slots') as SlotRegistry, describe, mutate,
setHostPreference: (next: string | undefined) => { preference = next; revision += 1 },
}
}
/** Stand in for the settings shell: declare the General item slot from root. */
function declareItems(slots: SlotsService): () => void {
function declareItems(slots: SlotRegistry): () => void {
return slots.register(
{ name: 'root', children: { [SLOT]: { kind: 'list', scope: 'root' } } } as never,
() => null,
@@ -64,7 +64,7 @@ function declareItems(slots: SlotsService): () => void {
/** Mirror the framework's inject choreography: bake a real instance from the
* declared handle and hand its actions to the entry's inject factory. */
function faceOf(slots: SlotsService) {
function faceOf(slots: SlotRegistry) {
const entry = slots.entries(SLOT).find(e => e.component === LanguageRow)!
const handle = entry.store as ReturnType<typeof createLanguageRowStore>
const instance = handle.create()
@@ -91,7 +91,7 @@ describe('locale apply', () => {
const before = await bench()
declareItems(before.slots)
await before.ctx.plugin({ inject: [...inject], apply }).await()
const locale = before.ctx.get('locale') as LocaleService
const locale = before.ctx.get('locale') as LocaleRuntime
// Base dictionaries are registered: the (ns, locale) seats are occupied.
expect(() => locale.register('common', 'zh', {})).toThrow('already has locale')
expect(() => locale.register('common', 'en', {})).toThrow('already has locale')
@@ -112,7 +112,7 @@ describe('locale apply', () => {
const b = await bench()
declareItems(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const locale = b.ctx.get('locale') as LocaleService
const locale = b.ctx.get('locale') as LocaleRuntime
// An event ahead of any inject hits the unbound-actions arm.
locale.setLocale('en')
@@ -136,7 +136,7 @@ describe('locale apply', () => {
b.setHostPreference('en')
declareItems(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const locale = b.ctx.get('locale') as LocaleService
const locale = b.ctx.get('locale') as LocaleRuntime
await vi.waitFor(() => { expect(locale.getLocale().active).toBe('en') })
b.setHostPreference(undefined)
b.ctx.remote.$dispatch('settings/document-updated', [LOCALE_SETTINGS_NAMESPACE, 0])

View File

@@ -1,11 +1,11 @@
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import { Settings, settingsNamespace, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
import { SettingsProvider, settingsNamespace, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
import {
LOCALE_SETTINGS_NAMESPACE, apply,
} from '@deepseek-ai/dsh-client-locale'
class MemorySettings extends Settings {
class MemorySettings extends SettingsProvider {
readonly writable = true
protected load(): Promise<Record<string, unknown>> { return Promise.resolve({}) }
protected persist(_ns: SettingsNamespace, _section: Record<string, unknown>): Promise<void> {

View File

@@ -2,16 +2,16 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { apply as nodeApply } from '@deepseek-ai/dsh-client-locale'
import { apply as clientApply, COMMON_NS, LocaleService, inject } from '@deepseek-ai/dsh-client-locale/client'
import { apply as clientApply, COMMON_NS, LocaleRuntime, inject } from '@deepseek-ai/dsh-client-locale/client'
import * as LocaleInvariant from '@deepseek-ai/dsh-client-locale/invariant'
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import InvariantService from '@deepseek-ai/dsh-invariants'
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
describe('invariant companion', () => {
it('registers under the package name with an empty installer', async () => {
const ctx = new Context()
await ctx.plugin(InvariantService, { enabled: true })
await ctx.plugin(InvariantRegistry, { enabled: true })
await expect(ctx.plugin(LocaleInvariant).await()).resolves.toBeDefined()
})
@@ -23,16 +23,16 @@ describe('invariant companion', () => {
// The feature registers its own Language settings row, hence the slots edge.
expect(inject).toEqual(['slots', 'connection', 'remote', 'settingsScope'])
const ctx = new Context()
new SlotsService(ctx)
new SlotRegistry(ctx)
ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never)
// The settings row's transport and the forwarded-event port.
ctx.provide('remote', { $on: () => () => {} } as never)
ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never)
await ctx.plugin({ inject, apply: clientApply }).await()
const locale = ctx.get('locale')
expect(locale).toBeInstanceOf(LocaleService)
expect(locale).toBeInstanceOf(LocaleRuntime)
// Seeded dictionaries occupy the (ns, locale) seats even while empty.
expect(() => (locale as LocaleService).register(COMMON_NS, 'zh', {})).toThrow('already has locale')
expect(() => (locale as LocaleService).register(COMMON_NS, 'en', {})).toThrow('already has locale')
expect(() => (locale as LocaleRuntime).register(COMMON_NS, 'zh', {})).toThrow('already has locale')
expect(() => (locale as LocaleRuntime).register(COMMON_NS, 'en', {})).toThrow('already has locale')
})
})

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