dsh-config-studio
A settings page for the part of DSH configuration the built-in pages never reach. It lists every addressable Loader entry in the active profile, renders a form from that plugin's own Config schema, and writes the fields you change into the profile's cordis.patch.yml — through ctx.configEditor, the same writer DSH's own settings use, but without the volatile-only restriction.
The page appears in Settings → Plugin configuration (设置 → 插件配置) and follows the Harness language (English / 简体中文).
Why this exists
DSH's built-in settings forms write through settings.mutate, which only accepts fields a plugin declared with .volatile(); writing anything else throws Config field "<path>" is not volatile. Everything else is expected to be edited by hand in YAML.
That restriction is reasonable for live-reloadable preferences, but it leaves most of a real profile without a UI: plugin lists, timeouts, feature switches, provider tables, and the rest. This plugin takes the other road — the complete raw config — and lets the Host validate and apply it exactly as the Loader would.
Features
- Browse — every entry
configEditor.entries()returns, with package name, field count, override badge, and current status. Search by id or package; switch between editable only (default) and all; group by function (models, agents, tools, web, accounts, runtime, sessions, interface, platform, other) or by origin (built-in / third-party); collapse sections; the browsing state survives opening an entry and coming back. - Edit — one field per top-level config key: text, number, switch, select for enums, and a JSON editor for objects and arrays. Each row shows the value in effect, whether it comes from the profile patch, and a one-click Reset back to the inherited value.
- Review, then save — a patch-shaped diff of what will be written, a revision check that refuses to overwrite a concurrent edit, and a result line that says whether the override was applied or removed.
- Read-only by design — credentials,
!!jsexpressions, unsupported schema shapes, and oversized entries are never writable here (table below). - Bilingual — English and Simplified Chinese, following the Harness language, including the nav label, every section, and error messages.
Install
The package is a dsh.bundle bundle with no dependencies and no build step, so every source below is equivalent. Pick the one that matches your host.
DSH Desktop (recommended)
Desktop profiles are managed by the application, so dsh plugin cannot install into them:
Open Settings → Plugins.
Paste one of the sources below into the install box:
https://github.com/MaxHan0102/dsh-config-studio/releases/download/v0.1.0/dsh-config-studio-0.1.0.tgzor
github:MaxHan0102/dsh-config-studioMake sure dsh-config-studio is enabled in the list, then reload the page (
Cmd/Ctrl+R).
dsh web or a custom profile
# Release tarball (pinned to a tag, so the URL never rots)
dsh plugin --profile web add https://github.com/MaxHan0102/dsh-config-studio/releases/download/v0.1.0/dsh-config-studio-0.1.0.tgz
# or straight from the repository
dsh plugin --profile web add github:MaxHan0102/dsh-config-studio
Replace web with your own profile name. Restart the host if it does not pick the plugin up, then reload the page.
Update
Re-run the install command with a newer tag:
dsh plugin --profile web add https://github.com/MaxHan0102/dsh-config-studio/releases/download/v0.1.1/dsh-config-studio-0.1.1.tgz
On Desktop, use the update control on the plugin's card. A git install has no automatic update check — remove and re-add it.
Remove
dsh plugin --profile web remove dsh-config-studio
Any override the plugin wrote stays in cordis.patch.yml; removing the plugin does not touch your configuration.
Requirements
| DSH | 0.2.1-rc.5 or newer (tested on 0.2.1-rc.5) |
| Services | ctx.configEditor and ctx.connection.fetch — both ship with the Web composition |
| Node | 20+ |
| Browser | any current Web GUI; the section is a normal client plugin |
The package deliberately declares no DSH version range: it is plain JavaScript with no host imports, so it keeps installing on newer hosts instead of being gated out by a prerelease peer range.
Usage
- Open Settings → Plugin configuration. The first screen lists the entries that have something to edit; switch the filter to All entries to inspect the read-only ones.
- Pick an entry. The pane shows its fields, generated from the plugin's own schema. A field marked Overridden comes from your profile patch; the rest show the inherited value.
- Edit. Changed rows are highlighted; invalid numbers, enums, and JSON are marked before you save. Reset on a row marks it to be restored to the inherited value (the pending value is shown in the field).
- Review. Save to profile opens a diff of exactly what will be written.
- Apply. The Host validates the complete config against the plugin's schema, writes the patch atomically, and reloads the entry through the Loader. A value the schema rejects comes back with the offending field named; a concurrent edit comes back as a conflict and offers a reload.
What the badges mean
| Badge | Meaning |
|---|---|
| Overridden (entry) | The profile patch has a config: record for this entry |
| Overridden (field) | This key is written in that record, so the value comes from the profile layer |
| Read-only | See the table below; the field is shown for context but will not be written |
| Third-party | The package is not under the @deepseek-ai/ scope |
Two behaviours worth knowing
A patch replaces the whole config. DSH applies a patch by replacing an entry's config object, not by merging keys. Saving one field therefore writes the entry's current config plus your change, and pins the other values it already had. If every field ends up equal to the inherited layer, the record is removed entirely — that is what Reset means for a whole entry.
Some changes reload the plugin. Every save goes through the Loader's normal lifecycle, so the plugin restarts with its new config. A few plugins only take effect after the host restarts; the page says so in the result line.
Read-only rules
Decided on the Host; the browser cannot bypass them.
| Condition | Reason |
|---|---|
role('secret') field |
the value never leaves the Host — the page only learns whether it is set |
role('credential-ref') field |
a reference is not an editable value |
| An object with a secret somewhere below it | editing it as JSON could leak the secret |
An unevaluated !!js expression |
only YAML can express it safely |
| A schema type the form does not understand | no safe control exists |
meta.disabled on the field |
the plugin disables it |
| Entry larger than 64 KiB | too large to edit responsibly in one form |
| Entry disabled, failed, inactive, or a group | not addressable right now |
Plugin declares no Config |
nothing to generate a form from |
How it works
Two halves, both in this repository:
Host (
index.js) — threePOSTendpoints registered throughctx.connection.fetch, so the/apifence, Host/Origin checks, and cookie authentication apply before dispatch. It imports nothing outside Node built-ins: the form shape is read from the liveschemasteryschema the Loader already holds.Endpoint Body Answers /api/config-studio/list{}entries, status, field counts, category, origin /api/config-studio/read{ id }revision, fields, values, read-only reason /api/config-studio/write{ id, expectedRevision, changes }{ ok, revision }or{ ok: false, error }Client (
client.js) — asettings.sectionregistration written in the module-loader format, with no Harness client package imports. Controls are the plugin's own, styled with theme tokens.
Writes are validated twice: once against the plugin's schema in the request, and again inside configEditor.edit(), which holds the profile lock, preserves comments and !!js, writes atomically, reloads the entry, and rolls the file back if the reload fails. revision is a hash of the entry's inherited layer, profile override, and raw config, so a stale form is refused instead of silently overwriting.
Troubleshooting
| Symptom | Fix |
|---|---|
| No Plugin configuration section | Confirm the plugin is enabled and active, then reload the page. On the Desktop app, check the plugin's card in Settings → Plugins. |
| "The configuration service did not answer." | The Host half is not active: check the DSH version, and that the entry is active in the plugin list. |
| A field you need is read-only | It matches one of the rules above; edit cordis.patch.yml directly for secrets and !!js. |
| Save succeeded but nothing changed | Another layer wins: a home patch (~/.dsh/cordis.patch.yml) or a --patch overlay takes precedence over the profile layer. |
| Save failed with a conflict | Someone (or another tool) wrote the entry after the page read it; reload the entry and re-apply. |
| The page vanished after a DSH update | Check the upstream release notes for the settings surface; open an issue with the DSH version. |
Security
- Only the active profile's
cordis.patch.ymlis ever written. Home patches, command-line overlays, and bundle layers are read but never modified. - Only entries returned by
configEditor.entries()are addressable; everything else is invisible to this page. - Secrets are never sent to the browser, and schema error messages are scrubbed of credential text before they leave the Host.
- Logs carry entry ids and field keys only — never configuration values.
- The browser cannot pick a different write target: the Host rebuilds the candidate config from its own copy and re-validates it.
Development
The repository is the installed artifact — plain JavaScript, no build.
index.js Host half: schema introspection, endpoints, validation
client.js Client half: settings section, form, diff, i18n
cordis.patch.yml Bundle patch that inserts the Host row
locale/*.json Display metadata for the plugins page
icon.svg Icon used by the plugin list
To run a checkout against a disposable profile:
cp -R ~/.dsh/profiles /tmp/dsh-home/profiles
DSH_HOME=/tmp/dsh-home dsh --profile web --no-open --port 0
Link the working copy into that profile (dsh plugin --profile web add <absolute path>) and reload the page after edits. The Host half is reloaded by re-enabling the plugin; the client half is re-fetched on page reload.
No comments yet. Be the first to write one.