Add `dsh-host-plugin-manager`: the `pluginManager` service and the `plugins` Remote that install, uninstall, enable, disable, and retry bundles over the booted profile, add and remove rows in the profile's or an agent preset's user layer, report what a package's services keep alive, and fold the manifest, the probe record, and the live tree into one view per package. pnpm runs the way the CLI runs it, streaming its output as `plugins/install-log`; every change emits `plugins/changed`, and api-remotes forwards both. Enabling is the Loader's own transaction: a rejected recomposition restores the layer list, and a runtime recomposition records contained rows through `recordContainedStates` because the boot audit does not run again; `ContainedGroup.create` now records a row that resolved pending rather than clearing it. Give every agent preset a user patch layer: `cordis.patch.yml` beside a locally authored composition, or alone in `.agent-presets/<id>/` for a shipped one. Discovery attaches and judges it, the mount applies it as the include's runtime patches, standing generations follow the layer's content and return to a retired generation when it comes back, the composition inventory reports each row's `source` and `disabledBy`, a copy carries the layer, and `removeOverlay` deletes it. `dsh-global-tool-mask` is `tools.restrict()` as a scope-only row for such a layer. Add `dsh-patch-file` as the one home of the patch-list format: the parser moved out of app-boot, plus a comment-preserving key-level writer with lock, atomic replace, and readback. app-boot exports `layerTrust`, `bundleLayerPatches`, and `recordContainedStates`, and `ProfileRuntime` carries the install anchor.
description, kind
| description | kind |
|---|---|
| Loader patch-list files: the shared parser for cordis.patch.yml layers and a comment-preserving key-level writer with lock, atomic replace, and readback. | package-reference |
@deepseek-ai/dsh-patch-file
English | 中文
Summary
dsh-patch-file owns the file format every composition layer of the harness is written in: a cordis.patch.yml is a top-level YAML sequence of Loader patch entries — id-targeted overrides such as disabled: true or a config replacement, and insert lists of new rows — in the Loader's own dialect, where a !!js scalar is an expression the row's fiber evaluates. parsePatchList is the one parser the profile launcher, bundle layers, --patch overlays, and agent-preset user layers read through, so a file one of them accepts is a file all of them accept. PatchDocument edits such a file at the key level while keeping its comments, blank lines, and !!js scalars as the author wrote them, and mutatePatchFile commits one edit under the cross-process writer lock, through an atomic replace, and reads the written text back through the parser before reporting what every reader will now load.
Table of Contents
- Use this package
- Understand the implementation
- Further Exploration
- Model Experience
- Known Limitations and Deferred Work
- Dev Note
Use this package
Read a patch layer with parsePatchList (text in hand) or readPatchListFile (absent file reads as undefined). Both anchor a relative insert row name such as ./plugin.js to the file's own directory and fail loud on anything that is not a sequence of mappings, because a patch file that cannot be applied at all is a misconfiguration; a patch whose target row is absent stays a per-entry Loader warning.
Write through mutatePatchFile. The callback receives a PatchDocument and edits it by row id, the way the Loader addresses rows:
await mutatePatchFile(file, (document) => {
document.setRowField('tool-web', 'disabled', true) // the id-targeted patch is created when absent
document.deleteRowField('tool-web', 'config') // a patch reduced to its id is removed whole
document.appendInsert({ id: 'tool-foo', name: 'dsh-tool-foo' }) // into the root list
document.appendInsert({ id: 'sql', name: 'dsh-sql' }, 'agents') // into the group with that id
document.removeInsert('tool-foo') // an emptied insert patch is removed whole
}, { binName: 'dsh', mode: 0o600, dirMode: 0o700 })
setRowField never accepts id or insert; rowField reads one key back, with a !!js scalar returned as its source text. appendInsert refuses an id the file already inserts, and insertedRow/removeInsert find rows inside inserted groups too. Values written are plain data; a !!js scalar on another key is left untouched, which is what lets a user layer revert a disabled: true it wrote without disturbing a bundle's !!js gate on a different row.
mutatePatchFile takes the <file>.lock sibling the way dsh-atomic-write does, reads the file (absent reads as empty), applies the edit, replaces the file atomically with the stated permission bits when the text changed, and returns the patch list as re-read from the written text. An edit that changes nothing writes nothing.
Understand the implementation
Implementation internals — click to expand
Two parsers, one dialect
Reading uses js-yaml with the include's entryListSchema, so !!js scalars become the expression nodes the Loader interpolates, exactly as the include mounts them. Writing uses the yaml package's comment-preserving Document: it keeps the unresolved !!js tag on the scalar it decorates (reported as a TAG_RESOLVE_FAILED warning, not an error) and prints it back verbatim, so an edit to one key never rewrites another key's expression. The written text is parsed back with the reading parser, which is the readback the writer's contract promises.
Addressing
An id-targeted patch is the top-level item whose id matches and that carries no insert. An inserted row is searched in every insert list, recursing into inserted groups (group: true with a config list). A top-level item that is not a mapping, or an insert whose value is not a list, fails the parse.
Source map
| File | Role |
|---|---|
src/index.ts |
parsePatchList, readPatchListFile, anchorInsertedPluginNames, PatchDocument, mutatePatchFile |
| — | No runtime invariant companion is published; the package holds no runtime state, and its file contract is pinned by unit tests. |
Further Exploration
Read these when the file format's place in the composition is the question.
- App boot — the profile launcher that composes bundle layers, user layers, and overlays from these files.
- Cordis include plugin —
applyEntryPatches, the patch semantics every layer is applied with. - Agent presets — the per-preset user layer written in this format.
Model Experience
None, as the package only reads and writes composition patch files; the rows they name own every model-facing registration.
KV Cache effect
None; this package neither assembles nor sends a provider request.
Known Limitations and Deferred Work
These limits define what the writer will not do to a file. They are current package constraints, not a task backlog.
- Key-level, not row-level, merging —
setRowField('x', 'config', value)replaces the wholeconfigmapping of that patch; a caller that wants one nested field changed reads the current value withrowFieldand writes the merged mapping back. - No expression authoring — the writer emits plain data only; a
!!jsgate is something an author types into the file, never something an API call produces. - Lock orphans are an operator action — a lock file left by a crashed writer is never removed by a contender, which fails after the wait instead;
dsh-atomic-writedocuments the same choice.
Dev Note
Working context for maintainers — click to expand
None.