Merge branch 'codex/disable-telemetry-default' into master

Resolved conflicts:
- packages/client/ui-settings-general/README.md, README.zh.md: kept PR
  opt-in telemetry description (DSH_TELEMETRY_MODE)
- scripts/snapshots/translation-prompt-v4: kept master's newer README
  structure snapshot
- i18n.yaml pairing records: resolved per file state
- pnpm-lock.yaml: regenerated
- Modify/delete conflicts (scaffold/telemetry, sdk-follow-up-capabilities):
  kept master deletions
This commit is contained in:
Chinesezjc
2026-08-12 17:27:37 +08:00
2021 changed files with 43956 additions and 23769 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/README.md
README.md: 98a856261bc632c97f350db8fc7bb0b10c22235d
README.zh.md: 283e54138e24202ed6b88d1d309538c58cd66b3e
README.md: 4fbd0692a2df403c6395235e096e193c994ea198
README.zh.md: 7861f5b4f8cb447aff01ec8b64b12fd298485b4b

View File

@@ -35,4 +35,4 @@ The [CLI behavior reference](reference/README.md) owns exact layer precedence, f
## Development
Production runs require built package and frontend artifacts. From the repository root, `pnpm dsh <args...>` builds those artifacts, runs the TypeScript entry, and forwards every argument; the [source-execution reference](reference/README.md#source-execution) owns the module-resolution contract.
Production runs require built package and frontend artifacts. From the repository root, run `pnpm run build` separately, then use `pnpm dsh <args...>` to run the TypeScript entry and forward every argument; the [source-execution reference](reference/README.md#source-execution) owns the module-resolution contract.

View File

@@ -35,4 +35,4 @@ profile 目录包含一个 `package.json`(树外插件依赖,加上 profile
## 开发
生产运行需要已构建的包与前端产物。仓库根目录运行 `pnpm dsh <args...>` 会先构建这些产物,再运行 TypeScript 入口并转发所有参数;模块解析约定由[源码执行参考](reference/README.md#source-execution)负责。
生产运行需要已构建的包与前端产物。请在仓库根目录单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>` 运行 TypeScript 入口并转发所有参数;模块解析约定由[源码执行参考](reference/README.md#source-execution)负责。

View File

@@ -60,6 +60,8 @@ flowchart LR
cfg --> plugin_dsh_base_sandbox_policy
plugin_dsh_base_bash_sandbox["bash-sandbox<br/>@deepseek-ai/dsh-bash-sandbox"]
cfg --> plugin_dsh_base_bash_sandbox
plugin_dsh_base_pwsh_sandbox["pwsh-sandbox<br/>@deepseek-ai/dsh-pwsh-sandbox"]
cfg --> plugin_dsh_base_pwsh_sandbox
plugin_dsh_base_approval["approval<br/>@deepseek-ai/dsh-user-approval"]
cfg --> plugin_dsh_base_approval
plugin_dsh_base_permission["permission<br/>@deepseek-ai/dsh-permission"]
@@ -68,6 +70,8 @@ flowchart LR
cfg --> plugin_dsh_base_bash_env
plugin_dsh_base_tool_bash["tool-bash<br/>@deepseek-ai/dsh-tool-bash"]
cfg --> plugin_dsh_base_tool_bash
plugin_dsh_base_tool_pwsh["tool-pwsh<br/>@deepseek-ai/dsh-tool-pwsh"]
cfg --> plugin_dsh_base_tool_pwsh
plugin_dsh_base_tool_tasks["tool-tasks<br/>@deepseek-ai/dsh-tool-tasks"]
cfg --> plugin_dsh_base_tool_tasks
plugin_dsh_base_fs_policy["fs-policy<br/>@deepseek-ai/dsh-fs-policy"]
@@ -194,10 +198,12 @@ flowchart LR
| `sandbox` | `@deepseek-ai/dsh-sandbox-local` |
| `sandbox-policy` | `@deepseek-ai/dsh-sandbox-policy` |
| `bash-sandbox` | `@deepseek-ai/dsh-bash-sandbox` |
| `pwsh-sandbox` | `@deepseek-ai/dsh-pwsh-sandbox` |
| `approval` | `@deepseek-ai/dsh-user-approval` |
| `permission` | `@deepseek-ai/dsh-permission` |
| `bash-env` | `@deepseek-ai/dsh-bash-env` |
| `tool-bash` | `@deepseek-ai/dsh-tool-bash` |
| `tool-pwsh` | `@deepseek-ai/dsh-tool-pwsh` |
| `tool-tasks` | `@deepseek-ai/dsh-tool-tasks` |
| `fs-policy` | `@deepseek-ai/dsh-fs-policy` |
| `tool-fs` | `@deepseek-ai/dsh-tool-fs` |

View File

@@ -45,11 +45,16 @@
# 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
# never reached the model's shell at all. `tool-bash` consumes the host registry
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
# sandbox policy owns it.
# never reached the model's shell at all. Both shell tools consume the host
# registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
# host-plane too.
- id: tool-bash
name: '@deepseek-ai/dsh-tool-bash'
disabled: !!js process.platform === 'win32'
- id: tool-pwsh
name: '@deepseek-ai/dsh-tool-pwsh'
disabled: !!js process.platform !== 'win32'
# ── filesystem ──────────────────────────────────────────────────────────────

View File

@@ -24,7 +24,7 @@
Two planes decide where an edit belongs. The HOST composition holds the registries and anything shared across sessions — persistence, the sandbox and approval stack, the model route, the subagent registry and its backends. An AGENT PRESET holds what one session contributes to those registries: its tools, its persona, its prompt sections. A row that publishes a service belongs in the host composition, or inside an `isolate` realm if the preset genuinely owns that service and nothing outside one agent reads it.
Presets you author live under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/`, one directory per preset. NEVER edit or delete the shipped preset install (the `agent-presets` directory beside the deployment's own config): it belongs to the deployment, an upgrade overwrites it, and corrupting the `cordis` preset would disable this very mode. To change what a shipped preset does, copy its composition into a new preset directory and edit the copy.
Presets you author live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/`; the roster reports each preset's real path, so take the one you edit from there. NEVER edit or delete the shipped preset install (the `agent-presets` directory beside the deployment's own config): it belongs to the deployment, an upgrade overwrites it, and corrupting the `cordis` preset would disable this very mode. To change what a shipped preset does, copy its composition into a new preset directory and edit the copy.
Load the `editing-cordis-compositions` skill before writing or changing a composition.
@@ -39,11 +39,16 @@
# 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
# never reached the model's shell at all. `tool-bash` consumes the host registry
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
# sandbox policy owns it.
# never reached the model's shell at all. Both shell tools consume the host
# registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
# host-plane too.
- id: tool-bash
name: '@deepseek-ai/dsh-tool-bash'
disabled: !!js process.platform === 'win32'
- id: tool-pwsh
name: '@deepseek-ai/dsh-tool-pwsh'
disabled: !!js process.platform !== 'win32'
# ── filesystem ──────────────────────────────────────────────────────────────

View File

@@ -1,12 +1,18 @@
---
name: editing-cordis-compositions
description: Use when creating or changing a Cordis composition for this harness — writing or editing an agent preset, adding or removing a plugin row, deciding whether something belongs to the host composition or to one session, or diagnosing a row that mounted but contributed nothing.
description: Use when creating, changing, or validating a Cordis composition for this harness — writing or editing an agent preset, adding or removing a plugin row, deciding whether something belongs to the host composition or to one session, checking whether a preset you authored actually mounts, or diagnosing a row that mounted but contributed nothing.
---
# Editing Cordis compositions
Every capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it.
## Off-limits
**Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation.
To change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete.
## Decide the plane first
Two planes, and the choice is not about how "agent-related" something feels — it is about whether the thing must be shared.
@@ -17,16 +23,105 @@ Two planes, and the choice is not about how "agent-related" something feels —
**A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side.
A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. The shipped presets live beside the deployment's composition; locally authored ones live under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/<name>/`.
A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name.
Locally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created.
## The roster service
`ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step.
Read `cordis_inspect what:"api" name:"agentPresets"` for the current signatures before writing the code. What this skill relies on:
- `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent.
- `read(id)` — one preset's composition text, without a file tool or a path.
- `copy(from, id, name?)` — the only authoring write (see below).
- `standingKeyFor(id)` — mount-validate one preset (see below).
```js
return {
name: 'preset-tools',
inject: ['agentPresets', 'tools'],
apply(ctx) {
harness.registerTool(ctx, harness.defineTool({
name: 'preset_check',
description: 'Mount-validate one preset by id.',
parameters: { id: { type: 'string', required: true } },
output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } },
async execute(args) {
try {
await ctx.agentPresets.standingKeyFor(args.id)
return 'mounted OK'
} catch (error) {
return error.message
}
},
}))
},
}
```
Unmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind.
## Authoring a preset
1. **Start from a copy.** Read a shipped composition close to what you want (the `standard` preset is the full coding agent) and copy its whole directory into `${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/` — the id must be lowercase letters, digits, and hyphens, because it becomes the directory name. A composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable.
2. **Expect the file sandbox.** The preset root lies outside the session workspace, so under the default `workspace-write` policy the first write is denied. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands.
3. **Rewrite `preset.yml`**: give the copy its own `name` and `description`, and drop any `order` the source declared — that field sorts the shipped roster.
4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and realm rule above.
1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source.
2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do.
3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`.
4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule.
5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*.
### Native product subagents
A composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable.
## The rule that catches people
**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later.
Whether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:"services"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service.
When a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here:
```yaml
- id: delegation
name: cordis:group
group: true
isolate:
workflows: true
config:
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
config:
provider: spawn
- id: tool-workflow
name: '@deepseek-ai/dsh-tool-workflow'
```
`true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs.
A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated.
Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-tasks`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.
## Verifying a change
**`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails:
- a row whose package does not resolve (`Cannot find package …`);
- a row whose config is invalid (`invalid config: $.<field> missing required value`);
- a row that never activated (`N row(s) did not activate: <id>: waiting for <service>`);
- a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) [<name>]; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service "<name>" has been registered at <Owner>`. Both name the offending service.
It returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind.
**Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition.
`cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do.
After a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces.
`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file.
## Native product subagents
Codex and Claude Code providers already live in the host composition. A preset chooses either product by contributing the same ordinary delegation-tool row used for spawn and fork; never move a product provider into the preset and never add a product-specific settings field.
@@ -54,43 +149,6 @@ Copy these disabled templates from a shipped full preset and remove `disabled` o
The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. The host must provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product.
The shipped preset directories are off-limits: never edit or delete them, and never escalate the sandbox to reach them, even when a change there looks quicker — an upgrade overwrites the install, and corrupting the `cordis` preset disables preset authoring itself. Locally authored presets under the user root are yours to create, edit, and delete.
## The rule that catches people
**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later.
Whether a row publishes a service is not visible from its name. `tool-bash` reads like a tool but provides `bashEnv`. Check the package's README, or mount the preset and read the rejection — it names the offending service.
When a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm:
```yaml
- id: tasks
name: cordis:group
group: true
isolate:
tasks: true
config:
- id: tasks-local
name: '@deepseek-ai/dsh-tasks-local'
- id: tool-tasks
name: '@deepseek-ai/dsh-tool-tasks'
```
`true` means a realm private to each mounting session. A string label instead pools one instance across every subtree naming that label — use it only for something genuinely expensive to duplicate.
A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. That is the quietest failure here: the mount succeeds and a tool is simply missing.
Host capabilities exposed through registries need no realm: the host `tools` and `skills` registries are layered per scope, so rows like `skill-local` and `tool-skill` sit loose in the preset and their registrations file into this preset's layer automatically — the agent's catalog merges them with whatever the deployment registered globally.
## Verifying a change
Read the live runtime with `cordis_inspect` — it reports the services, the plugin fibers, and the registered tools as they actually are, which is the only reliable check that a row did what its name suggests. Note it shows THIS session's composition: a preset you just wrote is not mounted anywhere until a session starts on it.
To check a preset you authored, re-read the files and validate these fields: the top level is a YAML list, every row is a map with a `name`, every group carries its own list, and service-publishing rows sit behind an `isolate` realm. The settings page's preset roster validates the same fields and marks an unloadable preset broken in red — point the user there, and ask them to start a session on the new preset to confirm the tool list; you cannot start one yourself.
`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file.
## What not to move into a preset
`agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement.

View File

@@ -1,8 +1,9 @@
# The `minimal` agent preset: a fixed-prompt, two-tool coding surface.
# The `minimal` agent preset: a fixed-prompt, two-tool coding-agent composition.
#
# The persona is the complete system prompt, so global identity, Web surface,
# The persona is the complete system prompt, so global identity, Web orientation,
# tool guidance, and later assembly listeners cannot add prompt text. The model
# composes only the persistent `bash` and `str_replace_editor` tools.
# composes only the persistent `bash` and `str_replace_editor` tools. Context
# compaction is deliberately absent.
- id: persona
name: '@deepseek-ai/dsh-persona'
@@ -41,33 +42,20 @@
* Please avoid commands that may produce a very large amount of output.
* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
# The editor requires absolute paths unconditionally.
- id: str-replace-editor
name: '@deepseek-ai/dsh-tool-str-replace-editor'
config:
maxOutputChars: 16000
# Model capacity comes from routed model metadata; this block states the
# compaction policy explicitly.
#
# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
# plane, and the row here resolves that one instance. It takes no configuration,
# keys every fold by Session, and owns the context-meter projection units the
# browser reads for every session — behind a realm those units would come and go
# with whichever presets happen to be mounted. What a preset chooses is whether
# its agent compacts at all, which is `compact-basic` below.
- id: compaction
# The bare local filesystem shadows the host's sandboxed provider only for this
# preset. The editor shares that realm and requires absolute paths.
- id: filesystem
name: cordis:group
group: true
isolate:
compact: true
fs: true
config:
- id: compact-basic
name: '@deepseek-ai/dsh-compact-basic'
- id: fs-local
name: '@deepseek-ai/dsh-fs-local'
config:
thresholdRatio: 0.8
retainTokens: 20480
summarizationProvider: ''
summarizationModel: ''
maxTokens: 8192
compactionRetries: 1
cwd: !!js process.env.DSH_CWD ?? process.cwd()
- id: str-replace-editor
name: '@deepseek-ai/dsh-tool-str-replace-editor'
config:
maxOutputChars: 16000

View File

@@ -38,11 +38,16 @@
# 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
# never reached the model's shell at all. `tool-bash` consumes the host registry
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
# sandbox policy owns it.
# never reached the model's shell at all. Both shell tools consume the host
# registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
# host-plane too.
- id: tool-bash
name: '@deepseek-ai/dsh-tool-bash'
disabled: !!js process.platform === 'win32'
- id: tool-pwsh
name: '@deepseek-ai/dsh-tool-pwsh'
disabled: !!js process.platform !== 'win32'
# ── filesystem ──────────────────────────────────────────────────────────────

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh",
"description": "dsh CLI: profile boot, plugin management, and the browser UI alias",
"version": "0.0.1-rc.1",
"version": "0.0.1-rc.2",
"publishConfig": {
"access": "restricted"
},
@@ -27,7 +27,6 @@
"@deepseek-ai/dsh-agent-tool-mode": "workspace:^",
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-base": "workspace:^",
"@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^",
"@deepseek-ai/dsh-command-compact": "workspace:^",
"@deepseek-ai/dsh-command-goal": "workspace:^",
"@deepseek-ai/dsh-compact-basic": "workspace:^",
@@ -36,6 +35,7 @@
"@deepseek-ai/dsh-goal-session": "workspace:^",
"@deepseek-ai/dsh-cmdline": "workspace:^",
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-headless": "workspace:^",
"@deepseek-ai/dsh-mcp-client": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
@@ -47,6 +47,7 @@
"@deepseek-ai/dsh-pwsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-time-context": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-local": "workspace:^",
"@deepseek-ai/dsh-tasks-local": "workspace:^",
@@ -61,6 +62,7 @@
"@deepseek-ai/dsh-tool-goal": "workspace:^",
"@deepseek-ai/dsh-tool-pwsh": "workspace:^",
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
"@deepseek-ai/dsh-tool-schedule": "workspace:^",
"@deepseek-ai/dsh-tool-skill": "workspace:^",
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
"@deepseek-ai/dsh-tool-subagent": "workspace:^",

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md
README.md: d34ecc4030812aae95884d6beded740646c6dd0a
README.zh.md: 56ae96c8dcab0146d18b6a9ecee236e22b20d9e2
README.md: 17d63fe73ea2b3bda74e9c4da91555b34c21f4ab
README.zh.md: ee99592fbba07b52b29db534e12c1d0ef23e5ef0

View File

@@ -24,7 +24,7 @@ The shipped apps own these command lines:
| Profile | Arguments |
|---|---|
| `web` | `--host`, `--port`, `--dev`, repeatable `--trusted-host` |
| `web` | `--host`, `--port`, repeatable `--trusted-host` |
| `headless` | the task text, as the positional argument |
A one-shot task (`dsh --profile headless "run the tests"`) creates one fresh persisted Agent through the core registry, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It prints the text on stdout and exits 0 for `completed`, else 1. An invocation with no task is a usage error from that app. The shipped headless profile mounts no ApiProxy, Host, HTTP server, Web runtime, or browser client; a successful run writes nothing to stderr and opens no listening port.
@@ -52,7 +52,7 @@ Git-hosted plugins that ship sources build during install through their `prepare
## Web alias
`dsh web` is a hardcoded alias for `--profile web`; the flags after it belong to the web app, whose ordinary bundle provider parses them. `--host` and `--port` override the composed values of the rows that carry them, repeatable `--trusted-host` contributes invocation authorities through `ctx.webRuntime.trustedHosts` (a deployment expression concatenates its own authorities), and `--dev` switches the web-runtime row to development mode and enables the client-plugin HMR receiver the bundle ships disabled; it expects a separate `pnpm run dev:web` watcher for no-refresh client bundle updates.
`dsh web` is a hardcoded alias for `--profile web`; the flags after it belong to the web app, whose ordinary bundle provider parses them. `--host` and `--port` override the composed values of the rows that carry them, and repeatable `--trusted-host` contributes invocation authorities through `ctx.webRuntime.trustedHosts` (a deployment expression concatenates its own authorities). The client-plugin HMR receiver is always mounted and stays idle until a separate `pnpm run dev:web` watcher rebuilds client bundles.
```sh
dsh web
@@ -63,9 +63,9 @@ dsh web --help
The production Web runner needs built package and frontend artifacts (`pnpm run build`). It serves `http://127.0.0.1:3080` by default. Binding all interfaces also trusts the machine's discovered LAN IP literals; `--trusted-host` adds named authorities accepted by the `/api` browser-trust fence.
Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed.
Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain`SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed.
All modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. Long-lived surfaces watch valid edits of both `cordis.patch.yml` layers (profile and home) and reapply them transactionally; one-shot runs read the files once at startup.
All modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. Every profile boot watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a one-shot surface exits through its bounded shutdown, which disposes the watchers.
New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads, network access, and process visibility are not confined. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one.
@@ -81,4 +81,4 @@ Install external plugin bundles through `dsh plugin --profile <name> add <packag
## Source execution
From the repository root, use `pnpm dsh <args...>`. The `package.json` script runs the complete repository build, launches `apps/cli/src/bin.ts` with `node --import tsx/esm`, and forwards every argument. Build output appears before CLI output. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.
From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing TypeRT host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.

View File

@@ -24,7 +24,7 @@
| Profile | 参数 |
|---|---|
| `web` | `--host``--port``--dev`可重复的 `--trusted-host` |
| `web` | `--host``--port`、可重复的 `--trusted-host` |
| `headless` | 任务文本,作为位置参数 |
一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent智能体提交任务、等待完全停稳并对 Session 执行 flush再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。
@@ -52,7 +52,7 @@ Git 托管、随附源码的插件在安装期间通过其 `prepare` 脚本构
## Web 别名
`dsh web``--profile web` 的硬编码别名;写在它之后的 flag 属于 web 应用,由组合包中的普通提供方解析。`--host``--port` 覆盖承载它们的那些行的组合取值,可重复的 `--trusted-host` 通过 `ctx.webRuntime.trustedHosts` 提供本次调用的 authority部署表达式会拼接自己的 authority`--dev` 把 web-runtime 行切换到开发模式并启用组合包以禁用状态交付的客户端插件 HMR热模块替换接收器若要无刷新更新客户端 bundle还需单独运行 `pnpm run dev:web` watcher。
`dsh web``--profile web` 的硬编码别名;写在它之后的 flag 属于 web 应用,由组合包中的普通提供方解析。`--host``--port` 覆盖承载它们的那些行的组合取值,可重复的 `--trusted-host` 通过 `ctx.webRuntime.trustedHosts` 提供本次调用的 authority部署表达式会拼接自己的 authority客户端插件 HMR热模块替换接收器始终挂载单独运行 `pnpm run dev:web` watcher 重建客户端 bundle 之前保持空闲
```sh
dsh web
@@ -63,9 +63,9 @@ dsh web --help
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。绑定所有接口时,还会信任机器自动发现的 LAN IP 字面量;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
进程关闭时会给插件树最多 5 秒完成 dispose。第一次 `SIGINT`/`SIGTERM` 启动该优雅排空;第二次信号强制立即退出。如果一次性运行正常结束时已经卡在 dispose 中,第一次 `Ctrl+C` 就会升格并立即退出,而不会被吞掉。
进程关闭时会给插件树最多 5 秒完成 dispose。第一次 `SIGINT`/`SIGTERM` 启动该优雅排空——`SIGTERM` 是监督进程的普通停止请求,在所有 surface 上以 0 退出,`SIGINT` 报告 130;第二次信号强制立即退出。如果一次性运行正常结束时已经卡在 dispose 中,第一次 `Ctrl+C` 就会升格并立即退出,而不会被吞掉。
所有模式都将调用目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md``CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。常驻 surface 监视两个 `cordis.patch.yml`profile 与 home的有效编辑并以事务方式重新应用一次性运行只在启动时读取这些文件一次
所有模式都将调用目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md``CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。每次 profile 启动都监视两个 `cordis.patch.yml`profile 与 home的有效编辑并以事务方式重新应用一次性 surface 经由有界关闭退出,关闭会先 dispose 监视器
新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取、网络访问和进程可见性不受限制。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。
@@ -81,4 +81,4 @@ dsh web --help
## 源码执行
仓库根目录使用 `pnpm dsh <args...>``package.json` 中的脚本会完成整个仓库的构建,通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。构建输出会显示在 CLI 输出之前。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY``HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>``package.json` 中的脚本会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。TypeRT Host 产物缺失时profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY``HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。

View File

@@ -15,7 +15,6 @@ import {
type ConfigDumpLayer,
} from '@deepseek-ai/dsh-app-boot'
import { homePatchPath, prepareProfile, PROFILE_ROOT_FILENAME } from './profile-boot.ts'
import { resolveWindowsShellLayer } from './windows-shell.ts'
const NAME = 'dsh'
@@ -34,12 +33,6 @@ export function runDumpConfig(profile: string, defaultOnly: boolean, patches: re
label: layer.packageName,
patches: layer.patches,
}))
// The win32 shell platform layer rides between bundles and user layers,
// exactly where the boot applies it.
const windowsShellLayer = resolveWindowsShellLayer(process.platform, loaded.layers, NAME)
if (windowsShellLayer !== undefined) {
layers.push({ label: windowsShellLayer.label, patches: windowsShellLayer.patches })
}
if (!defaultOnly) {
if (existsSync(loaded.patchPath)) {
layers.push({ label: loaded.patchPath, patches: loaded.patches })

View File

@@ -29,18 +29,14 @@ import {
watchUserPatches,
type Profile,
} from '@deepseek-ai/dsh-app-boot'
import { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-paths'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
/** Shipped agent-preset root: beside this app's own config, in both source and built layouts. */
const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', import.meta.url))
/** Harness-home directory holding locally authored agent presets. */
const USER_PRESET_DIR = '.agent-presets'
import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import type { HeadlessIo } from '@deepseek-ai/dsh-headless'
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
import { resolveWindowsShellLayer } from './windows-shell.ts'
const NAME = 'dsh'
@@ -60,9 +56,6 @@ export const INSTALL_ANCHOR = fileURLToPath(new URL('../package.json', import.me
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets. */
const TELEMETRY_ROW_ID = 'telemetry-otel'
/** The one-shot runner row: its presence means this composition exits by itself. */
const HEADLESS_ROW_ID = 'headless-runner'
/** The empty root entry list every profile tree patches over. */
const PROFILE_ROOT_CONFIG = `# dsh profile root — an empty entry list. The tree is composed as patches:
# each bundle in package.json's dsh.profile.bundles, then cordis.patch.yml, then any
@@ -114,8 +107,6 @@ interface ComposedProfile {
profile: Profile
/** Bundle layers concatenated — the part below the user layers on a live reload. */
bundlePatches: PatchOptions[]
/** The win32 shell platform layer (the base bundle's `windows.cordis.patch.yml`), between bundles and user layers. */
windowsShellPatches: PatchOptions[]
/** The home-level user layer (`$DSH_HOME/cordis.patch.yml`), applied after the profile's own. */
homePatches: PatchOptions[]
/** Layers above the user layers on a live reload: `--patch` overlays and the telemetry switch. */
@@ -131,7 +122,6 @@ interface ComposedProfile {
function allPatches(composed: ComposedProfile): PatchOptions[] {
return [
...composed.bundlePatches,
...composed.windowsShellPatches,
...composed.profile.patches,
...composed.homePatches,
...composed.overlays,
@@ -140,10 +130,10 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
/**
* Load `name` and compose its effective patch stack: bundle layers in
* `dsh.profile.bundles` order, the win32 shell platform layer (when the host
* is Windows), the profile's user layer, the home-level user layer
* (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply to
* every profile, so it outranks the per-profile layer), `--patch` overlays,
* `dsh.profile.bundles` order (the base bundle gates the shell stacks by
* platform on its own rows), the profile's user layer, the home-level user
* layer (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply
* to every profile, so it outranks the per-profile layer), `--patch` overlays,
* then the telemetry switch.
* @param name - the profile name.
* @param patchFiles - `--patch` overlay paths, in argv order.
@@ -157,28 +147,27 @@ function composeProfile(
const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
const bundlePatches = profile.layers.flatMap(layer => layer.patches)
const windowsShellPatches = resolveWindowsShellLayer(process.platform, profile.layers, NAME)?.patches ?? []
const rows = new Map<string, EntryOptions>()
for (const row of composeEntries([bundlePatches, windowsShellPatches, profile.patches, homePatches, overlays])) {
for (const row of composeEntries([bundlePatches, profile.patches, homePatches, overlays])) {
if (typeof row.id === 'string') rows.set(row.id, row)
}
const composedOverlays = [...overlays]
// Preset roots belong to every dsh composition that mounts the roster.
// The SHIPPED root is the part of the roster only this app can resolve: it
// sits beside this app's own config, in both the source and built layouts.
// The writable root the roster appends is `dsh-agent-presets`' own, so a
// launcher that never reaches this patch still finds a person's presets.
if (rows.has('agent-presets')) {
composedOverlays.push({
id: 'agent-presets',
config: {
...(rows.get('agent-presets')?.config ?? {}) as Record<string, unknown>,
roots: [
{ path: SHIPPED_PRESET_ROOT, trust: 'system' },
{ path: dshHomePath(USER_PRESET_DIR), trust: 'user' },
],
roots: [{ path: SHIPPED_PRESET_ROOT, trust: 'system' }],
},
})
}
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
if (telemetryPatch !== undefined) composedOverlays.push(telemetryPatch)
return { profile, bundlePatches, windowsShellPatches, homePatches, overlays: composedOverlays, rows }
return { profile, bundlePatches, homePatches, overlays: composedOverlays, rows }
}
/** Options for {@link runProfile}. */
@@ -193,9 +182,20 @@ export interface RunProfileOptions {
args: readonly string[]
}
/** Re-throw setup failures unless this invocation's signal already owns shutdown. */
function suppressSignalShutdownError(signal: AbortSignal, error: unknown): void {
if (!signal.aborted) throw error
/**
* Re-throw a watcher-setup failure unless a shutdown already owns the tree:
* a signal aborted this invocation, or an app requested exit (`ctx.appExit`
* from a fast one-shot) and the root's disposal rejected the in-flight setup
* await. Either way the failure describes a tree that is exiting as asked,
* not a broken watch.
* @param ctx - the booted root context.
* @param signal - this invocation's signal-shutdown fact.
* @param error - the setup failure.
*/
function suppressShutdownError(ctx: Context, signal: AbortSignal, error: unknown): void {
if (signal.aborted) return
if (ctx.fiber.state !== FiberState.ACTIVE || ctx.get('loader') === undefined) return
throw error
}
/**
@@ -206,11 +206,6 @@ function suppressSignalShutdownError(signal: AbortSignal, error: unknown): void
*/
export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Context; shutdown: ProcessShutdown }> {
const composed = composeProfile(options.profile, options.patchFiles)
// A one-shot composition ends by itself, which changes what a signal means
// and makes watching the user's patch layer pointless.
const headlessRow = composed.rows.get(HEADLESS_ROW_ID)
const oneShot = headlessRow !== undefined && headlessRow.disabled !== true
const app: { current?: Context } = {}
const shutdown = createProcessShutdown(async () => { await app.current?.fiber.dispose() })
const signalShutdown = new AbortController()
@@ -220,7 +215,10 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
}
// Signals own teardown throughout the startup window, not only after boot()
// settles: an inserted provider can publish before sibling rows finish mounting.
process.on('SIGTERM', () => { interrupt(oneShot ? 143 : 0) })
// SIGTERM is a supervisor's ordinary stop request and exits 0 on every
// surface — the launcher does not know whether the app considered its work
// complete; SIGINT is a user interrupt and reports 130.
process.on('SIGTERM', () => { interrupt(0) })
process.on('SIGINT', () => { interrupt(130) })
installFailLoud(NAME, process, async () => {
await app.current?.fiber.dispose()
@@ -241,14 +239,10 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
// removing the override could never revert the row to the bundle default.
const composeLive = (): PatchOptions[] => structuredClone([
...composed.bundlePatches,
...composed.windowsShellPatches,
...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [],
...loadOptionalPatches(NAME, homePatchPath()) ?? [],
...composed.overlays,
])
// One-shot runs exit through the runner; watching would only hold the
// process open after its exit request.
const watchProfilePatch = !oneShot
// Cloned for the same insert-aliasing reason as composeLive: the boot
// application must not mutate the objects later reloads recompose from.
const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), (hostCtx) => {
@@ -262,22 +256,16 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
args: options.args,
exit: code => void shutdown.shutdown(code),
})
if (oneShot) {
const io: HeadlessIo = {
stdout: process.stdout,
stderr: process.stderr,
exit: (code) => { void shutdown.shutdown(code) },
}
hostCtx.provide('headlessIo', io)
}
})
app.current = ctx
// A surface can dispose the whole tree while boot or this post-boot watcher
// setup is still in flight. Loader presence and fiber state own
// liveness; the local signal fact distinguishes that expected exit race
// from a real HMR error.
if (watchProfilePatch
&& !signalShutdown.signal.aborted
// setup is still in flight — a signal, or a fast one-shot's appExit. Loader
// presence and fiber state own liveness; the initial check skips a tree
// that already exited, and the catch below re-checks for an exit that
// landed mid-setup. Watching is unconditional: a one-shot surface exits
// through its bounded shutdown, which disposes the watchers before the
// loop drains.
if (!signalShutdown.signal.aborted
&& ctx.fiber.state === FiberState.ACTIVE
&& ctx.get('loader') !== undefined) {
try {
@@ -305,7 +293,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
compose: composeLive,
})
} catch (error) {
suppressSignalShutdownError(signalShutdown.signal, error)
suppressShutdownError(ctx, signalShutdown.signal, error)
}
}
return { ctx, shutdown }

View File

@@ -1,52 +0,0 @@
/**
* The Windows shell platform layer: on win32 hosts the shipped profile
* compositions swap the POSIX-only bash stack for the sandbox-confined
* PowerShell stack (`@deepseek-ai/dsh-pwsh-sandbox` +
* `@deepseek-ai/dsh-tool-pwsh`). The layer is the base bundle's
* `windows.cordis.patch.yml`, injected by the launcher between the bundle
* layers and the user layers so a user patch can still override it — the
* only override channel is composition config, like every other roster
* decision. POSIX hosts never receive the layer.
* @module @deepseek-ai/dsh/windows-shell
*/
import { join } from 'node:path'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { loadOverlayPatches, type ProfileLayer } from '@deepseek-ai/dsh-app-boot'
/** The base bundle whose package carries the Windows shell patch. */
export const BASE_BUNDLE = '@deepseek-ai/dsh-base'
/** The Windows shell patch filename inside the base bundle package. */
export const WINDOWS_SHELL_PATCH_FILENAME = 'windows.cordis.patch.yml'
/** One Windows shell platform layer: its patch file and parsed patches. */
export interface WindowsShellLayer {
/** The patch file path, used as the config-dump provenance label. */
label: string
/** The parsed patch entries, applied after the bundle layers. */
patches: PatchOptions[]
}
/**
* Resolve the Windows shell platform layer for a profile composition.
* @param platform - the host platform (`process.platform` at call sites).
* @param layers - the profile's bundle layers, in application order.
* @param binName - the diagnostic prefix on thrown errors (`dsh`).
* @returns the pwsh layer on win32, else `undefined`. A custom profile that
* mounts no base bundle is skipped (it owns its shell stack); a base
* bundle whose Windows shell patch is missing fails loud in
* {@link loadOverlayPatches} — the shipped package always carries it, so
* a miss is a broken installation.
*/
export function resolveWindowsShellLayer(
platform: NodeJS.Platform,
layers: readonly ProfileLayer[],
binName: string,
): WindowsShellLayer | undefined {
if (platform !== 'win32') return undefined
const base = layers.find(layer => layer.packageName === BASE_BUNDLE)
if (base === undefined) return undefined
const label = join(base.packageDir, WINDOWS_SHELL_PATCH_FILENAME)
return { label, patches: loadOverlayPatches(binName, label) }
}

View File

@@ -227,8 +227,8 @@ function createStartupFixture(): StartupFixture {
"export const inject = ['cmdlineArgs']",
'export function apply(ctx) {',
" const program = new Command().name('fixture').option('--generation <value>', 'echoed generation')",
' const values = parseCmdline(ctx, program, parsed => ({ generation: parsed.opts().generation }))',
' if (values !== undefined) ctx.provide(\'fixtureStartup\', values)',
" program.action(() => ctx.provide('fixtureStartup', { generation: program.opts().generation }))",
' parseCmdline(ctx, program)',
'}',
'',
].join('\n'))
@@ -408,16 +408,15 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
}
}, 30_000)
it('uses the Harness-home environment and managed credential through the published entry', async () => {
it('uses the launching endpoint and managed credential through the published entry', async () => {
const apiKey = 'built-home-layer-key'
const server = await startMockLlmServer({
sequence: ['success'],
apiKey,
successText: 'home environment reached the mock',
successText: 'launching endpoint reached the mock',
})
const home = mkdtempSync(join(tmpdir(), 'dsh-home-environment-'))
const project = mkdtempSync(join(tmpdir(), 'dsh-home-project-'))
writeFileSync(join(home, '.env'), `DEEPSEEK_BASE_URL=${server.baseURL}\n`)
writeFileSync(join(home, '.credentials.yaml'), `DEEPSEEK_API_KEY: ${apiKey}\n`, { mode: 0o600 })
createEnvironmentProbeProfile(home, project)
try {
@@ -427,7 +426,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
DSH_HOME: home,
DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: undefined,
DEEPSEEK_BASE_URL: undefined,
DEEPSEEK_BASE_URL: server.baseURL,
},
project,
)
@@ -435,7 +434,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
result.code,
`${result.stderr}\nstdout:\n${result.stdout}\nmock requests: ${String(server.requests.length)}`,
).toBe(0)
expect(result.stdout).toBe('home environment reached the mock')
expect(result.stdout).toBe('launching endpoint reached the mock')
expect(result.stdout).not.toContain(apiKey)
expect(result.stderr).not.toContain(apiKey)
expect(server.requests).toHaveLength(1)

View File

@@ -6,7 +6,7 @@ import { describe, expect, it } from 'vitest'
/**
* Keyless smoke for SOURCE `dsh` execution: run `apps/cli/src/bin.ts`
* with the exact production runtime vector (`node --import tsx/esm`, the
* vector the root `dsh` script invokes after building) and assert the
* vector the root `dsh` script invokes directly) and assert the
* required-config diagnostic. The Node compatibility matrix runs this
* WHOLE file, so a Node release changing module hooks or TypeScript handling
* breaks this gate instead of every developer's `pnpm dsh`; the built-bin
@@ -17,11 +17,11 @@ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const dshSourceBin = 'apps/cli/src/bin.ts'
describe('dsh SOURCE launcher (node --import tsx/esm)', () => {
it('builds before launching the source CLI', async () => {
it('launches the source CLI without building', async () => {
const rootPackage = JSON.parse(await readFile(new URL('../../../package.json', import.meta.url), 'utf8')) as {
readonly scripts?: Record<string, string>
}
expect(rootPackage.scripts?.dsh).toBe('pnpm run build && node --import tsx/esm apps/cli/src/bin.ts')
expect(rootPackage.scripts?.dsh).toBe('node --import tsx/esm apps/cli/src/bin.ts')
})
it('boots the source entry and requires a profile', async () => {

View File

@@ -14,7 +14,7 @@ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { resolveSessionPreset, SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets'
import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent'
import { CallId } from '@deepseek-ai/dsh-llm'
import type { BasicCompactService } from '@deepseek-ai/dsh-compact-basic'
import type {} from '@deepseek-ai/dsh-compact-basic'
import type {} from '@deepseek-ai/dsh-skill'
import type {} from '@deepseek-ai/dsh-tools'
// Type-only: resolves `ctx.get('sessionProjections')` and `ctx.get('tokenMeter')`.
@@ -79,18 +79,28 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
{ id: 'skill-badge', disabled: false },
{ id: 'modules', disabled: true },
{ id: 'connection', disabled: true },
// The always-on reload chain waits for the browser roster and bound port
// disabled above.
{ id: 'client-hmr', disabled: true },
// The shipped `-auto` chooser resolves its interaction from a running
// host and so waits for the webserver disabled above; the browse variant
// supplies `directoryPicker` without one.
{ id: 'directory-picker', disabled: true },
{ insert: [{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' }] },
{ insert: [
{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' },
{ id: 'ui-directory-picker', name: '@deepseek-ai/dsh-client-ui-directory-picker' },
] },
// The roster AppCLIEntry would patch in; only the shipped root, so a
// developer's own `~/.dsh/.preset` cannot change this test's outcome.
// `default` here is the COMPOSITION default — the base layer the settings
// document overrides.
{
id: 'agent-presets',
config: { default: 'standard', roots: [{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' }] },
config: {
default: 'standard',
roots: [{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' }],
includeUserRoot: false,
},
},
...extra,
]
@@ -213,16 +223,8 @@ describe('the shipped Web composition', () => {
expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION)
expect(JSON.stringify(assembly.tools.find(tool => tool.name === 'str_replace_editor')?.parameters))
.toContain('Absolute path')
const compact = ctx.agentPresets.serviceFor(handle.agent, 'compact')
expect(compact).toBeDefined()
expect((compact as BasicCompactService).config).toMatchObject({
thresholdRatio: 0.8,
retainTokens: 20480,
summarizationProvider: '',
summarizationModel: '',
maxTokens: 8192,
compactionRetries: 1,
})
expect(ctx.agentPresets.serviceFor(handle.agent, 'compact')).toBeUndefined()
expect(handle.agent.ctx.get('compact')).toBeUndefined()
} finally {
await handle.dispose()
}
@@ -444,6 +446,7 @@ describe('product subagent rows in user presets', () => {
{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
{ path: userRoot, trust: 'user' },
],
includeUserRoot: false,
},
}])
}, 120_000)
@@ -626,6 +629,66 @@ describe('a delegated child', () => {
})
})
describe('a launcher that configures no writable root', () => {
// The claim this default exists for, asserted through the real shipped
// bundles rather than a hand-built context: `apps/cli` patches in only the
// system root, and a person's own presets are found anyway because the
// roster derives `<dshHome>/.agent-presets` itself. `$DSH_HOME` is pointed
// at a temp home BEFORE boot — the derived root is resolved when the plugin
// is constructed, and an unpinned run would read the developer's own.
let derivedCtx: Context
let previousHome: string | undefined
beforeAll(async () => {
const home = await mkdtemp(join(tmpdir(), 'dsh-preset-derived-'))
previousHome = process.env.DSH_HOME
process.env.DSH_HOME = home
await mkdir(join(home, '.agent-presets', 'derived-mine'), { recursive: true })
await writeFile(
join(home, '.agent-presets', 'derived-mine', 'agent.cordis.yml'),
'- id: tool-todo\n name: \'@deepseek-ai/dsh-tool-todo\'\n config:\n allowParallelInProgress: true\n',
)
const settingsFile = join(await mkdtemp(join(tmpdir(), 'dsh-preset-derived-settings-')), 'settings.yaml')
await writeFile(settingsFile, '{}\n')
// Only the shipped root, exactly what `composeProfile` supplies; the
// writable one is the roster's own default rather than this patch's job.
derivedCtx = await bootWeb(settingsFile, [{
id: 'agent-presets',
config: {
default: 'standard',
roots: [{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' }],
includeUserRoot: true,
},
}])
}, 120_000)
afterAll(async () => {
if (previousHome === undefined) delete process.env.DSH_HOME
else process.env.DSH_HOME = previousHome
await derivedCtx.fiber.dispose()
})
it('discovers and mounts a preset the person authored under the harness home', async () => {
const listed = await derivedCtx.agentPresets.list()
const mine = listed.find(preset => preset.id === 'derived-mine')
expect(mine).toMatchObject({ trust: 'user' })
// Omitted rather than undefined: a healthy row carries no `broken` key.
expect(mine?.broken).toBeUndefined()
expect(derivedCtx.agentPresets.authorable).toBe(true)
const handle = await derivedCtx.agents.create({
sessionId: SessionId('preset-derived-root'),
setup: agentCtx => derivedCtx.agentPresets.mount(agentCtx, 'derived-mine').then(() => undefined),
})
try {
expect(toolNames(derivedCtx, handle.agent)).toContain('todo_write')
} finally {
await handle.dispose()
}
})
})
describe('authoring a preset on the shipped composition', () => {
let authorCtx: Context
let userRoot: string
@@ -644,6 +707,7 @@ describe('authoring a preset on the shipped composition', () => {
// nothing is the normal first-run state.
{ path: userRoot, trust: 'user' },
],
includeUserRoot: false,
},
}])
})

View File

@@ -1,73 +1,38 @@
/**
* The shipped shell composition: the base bundle gates both shell stacks by
* platform on its own rows (`disabled: !!js process.platform`), so exactly
* one shell stack mounts per host and no separate platform layer exists —
* the launcher applies nothing beyond the bundle layers. The spec composes
* the REAL shipped bundle layers (dsh-base + dsh-web-app resolved from the
* app installation anchor) through the boot's patch algorithm and pins the
* effective per-platform roster, the preset-level gates that keep tool-bash
* out of win32 sessions and tool-pwsh out of POSIX sessions, and the
* cold-start resolution closure for the pwsh rows' bare plugin names.
*/
import { afterEach, describe, expect, it } from 'vitest'
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, readFileSync } from 'node:fs'
import { mkdtempSync, rmSync, readFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { ProfileLayer } from '@deepseek-ai/dsh-app-boot'
import yaml from 'js-yaml'
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
import { evaluate } from '@deepseek-ai/cordis-plugin-loader'
import { composeEntries, initProfile, loadProfile, PROFILES_DIR } from '@deepseek-ai/dsh-app-boot'
import {
BASE_BUNDLE,
resolveWindowsShellLayer,
WINDOWS_SHELL_PATCH_FILENAME,
} from '../src/windows-shell.ts'
const WINDOWS_PATCH = `- id: bash-sandbox
disabled: true
- insert:
- id: pwsh-sandbox
name: '@deepseek-ai/dsh-pwsh-sandbox'
`
/** One fake bundle layer rooted in a temp directory. */
function fakeLayer(packageName: string, dir: string): ProfileLayer {
return { packageName, packageDir: dir, patchPath: join(dir, 'cordis.patch.yml'), patches: [] }
}
/** A base bundle layer whose package carries the Windows shell patch. */
function baseLayerWithPatch(dir: string): ProfileLayer {
writeFileSync(join(dir, WINDOWS_SHELL_PATCH_FILENAME), WINDOWS_PATCH)
return fakeLayer(BASE_BUNDLE, dir)
}
describe('resolveWindowsShellLayer', () => {
let base: string
afterEach(() => { if (base !== undefined) rmSync(base, { recursive: true, force: true }) })
const tempBase = (): string => {
base = mkdtempSync(join(tmpdir(), 'dsh-windows-shell-'))
return base
/**
* The effective disabled state of one row on one platform: a `!!js` expression
* evaluates with a platform-scoped `process` so both outcomes pin on any host.
*/
function disabledOn(row: { disabled?: unknown }, platform: 'win32' | 'linux'): boolean {
const value = row.disabled
if (value !== null && typeof value === 'object' && '__jsExpr' in value) {
return Boolean(evaluate({ process: { platform } }, (value as { __jsExpr: string }).__jsExpr))
}
return value === true
}
it('never applies on POSIX hosts', () => {
expect(resolveWindowsShellLayer('linux', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
expect(resolveWindowsShellLayer('darwin', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
})
it('defaults Windows hosts to the pwsh platform layer', () => {
const layer = resolveWindowsShellLayer('win32', [baseLayerWithPatch(tempBase())], 'dsh')
expect(layer).toBeDefined()
expect(layer?.label.endsWith(WINDOWS_SHELL_PATCH_FILENAME)).toBe(true)
expect(layer?.patches).toEqual([
{ id: 'bash-sandbox', disabled: true },
{ insert: [{ id: 'pwsh-sandbox', name: '@deepseek-ai/dsh-pwsh-sandbox' }] },
])
})
it('skips custom profiles without a base bundle', () => {
const other = fakeLayer('@deepseek-ai/dsh-custom', tempBase())
expect(resolveWindowsShellLayer('win32', [other], 'dsh')).toBeUndefined()
})
it('fails loud when the base bundle ships no Windows shell patch', () => {
const base = tempBase()
mkdirSync(base, { recursive: true })
// The overlay loader owns the fail-loud contract: the caller named this
// file, so its absence is a misconfiguration, not "no overlay".
expect(() => resolveWindowsShellLayer('win32', [fakeLayer(BASE_BUNDLE, base)], 'dsh'))
.toThrow(/dsh: failed to read overlay .*windows\.cordis\.patch\.yml/)
})
})
describe('the shipped Windows composition (real bundle layers)', () => {
describe('the shipped shell composition (real bundle layers)', () => {
let home: string
afterEach(() => { if (home !== undefined) rmSync(home, { recursive: true, force: true }) })
// The app installation anchor, mirroring profile-boot.ts: the bundle layers
@@ -75,65 +40,98 @@ describe('the shipped Windows composition (real bundle layers)', () => {
// suite composes the shipped patch files, not test fixtures.
const anchor = fileURLToPath(new URL('../package.json', import.meta.url))
it('composes the win32 confined roster through the real patch layers', () => {
it('composes the confined pwsh roster on win32 and the bash roster on POSIX from the same rows', () => {
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
const profile = loadProfile('dsh', 'web', anchor, home)
const warnings: string[] = []
const win32 = resolveWindowsShellLayer('win32', profile.layers, 'dsh')
expect(win32).toBeDefined()
const rows = composeEntries(
[...profile.layers.map(layer => layer.patches), win32!.patches],
profile.layers.map(layer => layer.patches),
message => warnings.push(message),
)
const byId = new Map(rows.map(row => [row.id, row]))
// Only the POSIX bash stack leaves the roster: the permission surface
// (sandbox/sandbox-policy/fs-sandbox, permission, approval) stays enabled
// exactly as on POSIX — the confined pwsh executor is what changes.
for (const id of ['bash-sandbox', 'tool-bash']) {
expect(byId.get(id)?.disabled, `row ${id}`).toBe(true)
// One shared patch set, two rosters: the shell stacks gate themselves.
for (const id of ['bash-sandbox', 'pwsh-sandbox', 'tool-bash', 'tool-pwsh']) {
expect(byId.has(id), `row ${id}`).toBe(true)
}
expect(disabledOn(byId.get('bash-sandbox')!, 'win32'), 'bash-sandbox on win32').toBe(true)
expect(disabledOn(byId.get('bash-sandbox')!, 'linux'), 'bash-sandbox on linux').toBe(false)
expect(disabledOn(byId.get('pwsh-sandbox')!, 'win32'), 'pwsh-sandbox on win32').toBe(false)
expect(disabledOn(byId.get('pwsh-sandbox')!, 'linux'), 'pwsh-sandbox on linux').toBe(true)
// Host shell-tool rows are disabled on every platform; sessions mount
// their own rows instead.
expect(byId.get('tool-bash')?.disabled).toBe(true)
expect(byId.get('tool-pwsh')?.disabled).toBe(true)
// The permission surface never moves: the sandbox/policy rows, the
// permission switcher, fs-sandbox, and the approval service stay enabled
// exactly as on POSIX — the confined pwsh executor is what changes.
for (const id of ['permission', 'ui-permission', 'sandbox', 'sandbox-policy', 'fs-sandbox', 'approval']) {
expect(byId.get(id)?.disabled, `row ${id}`).not.toBe(true)
}
for (const id of ['pwsh-sandbox', 'tool-pwsh']) {
expect(byId.has(id), `inserted row ${id}`).toBe(true)
}
// The launcher's cold-start module fallback BFS-links the apps/cli
// dependency closure into the profile's node_modules (the pwsh-local
// precedent), so every inserted bare plugin must resolve from there.
// dependency closure into the profile's node_modules, so every bare
// plugin name in the base patch must resolve from there.
const cliManifest = JSON.parse(readFileSync(anchor, 'utf8')) as { dependencies?: Record<string, string> }
for (const name of ['@deepseek-ai/dsh-pwsh-sandbox', '@deepseek-ai/dsh-tool-pwsh']) {
expect(cliManifest.dependencies?.[name], `cold-start closure must reach ${name}`).toBeDefined()
}
// The patch touches only base-owned rows plus inserts, so the full web
// profile composes without any no-match warning.
expect(warnings).toEqual([])
})
it('leaves POSIX untouched and base-only profiles compose without warnings', () => {
it('base-only profiles carry both stacks with the same platform gating', () => {
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
const profile = loadProfile('dsh', 'web', anchor, home)
// POSIX: no platform layer, the bash stack stays enabled.
const posixRows = composeEntries(profile.layers.map(layer => layer.patches))
const posixById = new Map(posixRows.map(row => [row.id, row]))
expect(posixById.get('bash-sandbox')?.disabled).not.toBe(true)
expect(posixById.has('pwsh-local')).toBe(false)
expect(posixById.has('pwsh-sandbox')).toBe(false)
// A base-only custom profile (the DEFAULT_PROFILE_BUNDLES template): the
// patch touches only base-owned rows (bash-sandbox/tool-bash) plus its
// inserts, so the composition produces no no-match warning.
initProfile(join(home, PROFILES_DIR, 'base-only'), ['@deepseek-ai/dsh-base'])
const baseOnly = loadProfile('dsh', 'base-only', anchor, home)
const baseWarnings: string[] = []
const win32 = resolveWindowsShellLayer('win32', baseOnly.layers, 'dsh')
expect(win32).toBeDefined()
composeEntries(
[...baseOnly.layers.map(layer => layer.patches), win32!.patches],
message => baseWarnings.push(message),
const profile = loadProfile('dsh', 'base-only', anchor, home)
const warnings: string[] = []
const rows = composeEntries(
profile.layers.map(layer => layer.patches),
message => warnings.push(message),
)
expect(baseWarnings).toEqual([])
const byId = new Map(rows.map(row => [row.id, row]))
for (const id of ['bash-sandbox', 'tool-bash', 'pwsh-sandbox', 'tool-pwsh']) {
expect(byId.has(id), `row ${id}`).toBe(true)
}
// No web overlay: the tool rows keep their own gating too.
expect(disabledOn(byId.get('tool-bash')!, 'win32'), 'tool-bash on win32').toBe(true)
expect(disabledOn(byId.get('tool-bash')!, 'linux'), 'tool-bash on linux').toBe(false)
expect(disabledOn(byId.get('tool-pwsh')!, 'win32'), 'tool-pwsh on win32').toBe(false)
expect(disabledOn(byId.get('tool-pwsh')!, 'linux'), 'tool-pwsh on linux').toBe(true)
expect(warnings).toEqual([])
})
})
describe('shipped agent presets gate both shell tools by platform', () => {
const presetRoot = resolve(fileURLToPath(new URL('../package.json', import.meta.url)), '..', 'config', 'agent-presets')
it.each(['standard', 'code', 'cordis'])('preset %s gates its shell tool rows by platform', (preset) => {
const entries: unknown = yaml.load(
readFileSync(join(presetRoot, preset, 'agent.cordis.yml'), 'utf8'),
{ schema: entryListSchema },
)
if (!Array.isArray(entries)) throw new TypeError(`preset ${preset} must parse to an entry array`)
for (const [id, win32] of [['tool-bash', true], ['tool-pwsh', false]] as const) {
const row = entries.find((entry): entry is Record<string, unknown> => (
typeof entry === 'object' && entry !== null && (entry as Record<string, unknown>).id === id
))
if (row === undefined) throw new TypeError(`preset ${preset} must mount ${id}`)
expect(row.disabled).toMatchObject({ __jsExpr: expect.any(String) as string })
// A platform-scoped context pins both outcomes on every host.
const expression = (row.disabled as { __jsExpr: string }).__jsExpr
expect(Boolean(evaluate({ process: { platform: 'win32' } }, expression)), `${id} on win32`).toBe(win32)
expect(Boolean(evaluate({ process: { platform: 'linux' } }, expression)), `${id} on linux`).toBe(!win32)
}
})
it('minimal mounts no shell tool row at all (its shell is the PTY stack)', () => {
const entries: unknown = yaml.load(
readFileSync(join(presetRoot, 'minimal', 'agent.cordis.yml'), 'utf8'),
{ schema: entryListSchema },
)
if (!Array.isArray(entries)) throw new TypeError('minimal preset must parse to an entry array')
for (const id of ['tool-bash', 'tool-pwsh']) {
expect(entries.some(entry => (
typeof entry === 'object' && entry !== null && (entry as Record<string, unknown>).id === id
)), `${id} must be absent from minimal`).toBe(false)
}
})
})

View File

@@ -70,51 +70,6 @@
},
{
"path": "../../packages/bash/tool-bash"
},
{
"path": "../../packages/client/connection"
},
{
"path": "../../packages/client/hmr"
},
{
"path": "../../packages/client/runtime"
},
{
"path": "../../packages/client/ui-theme"
},
{
"path": "../../packages/client/ui-settings"
},
{
"path": "../../packages/client/ui-settings-general"
},
{
"path": "../../packages/client/ui-models"
},
{
"path": "../../packages/client/ui-permission"
},
{
"path": "../../packages/client/locale"
},
{
"path": "../../packages/client/ui-layout"
},
{
"path": "../../packages/client/ui-sidebar"
},
{
"path": "../../packages/client/ui-conversation"
},
{
"path": "../../packages/client/ui-plan"
},
{
"path": "../../packages/client/ui-trajectory"
},
{
"path": "../../packages/client/ui-question"
}
]
}