README.md 23 KB


description: "Shared Loader boot support for dsh profiles and the temporary Python SDK runtime: environment layers, patches, diagnostics, and configuration preview."

kind: "package-library"

@deepseek-ai/dsh-app-boot

English | 中文

Summary

dsh-app-boot is the shared Loader boot library behind dsh profiles, including the CLI packaged by the Python runtime wheel. It loads environment layers, composes profile bundles and patches, boots every plugin, and returns the running app or identifies the failed plugin and cause. Product applications use the dsh launcher instead of publishing separate bins; direct-config helpers remain only for lower-level embedders and tests. You can preview the effective configuration before booting, select live or startup-only patch application per profile, and let a terminal-owning app restore its terminal before a fatal exit.

Table of Contents


Use this package

Starting an app with this package is a small, explicit entry point: you give it a config file and it runs the whole boot. This section covers what you can do and what you get; the helper calls behind each outcome are documented in the folded implementation section.

When to use it

Use it when implementing the shared dsh launcher or embedding its lower-level boot helpers. Product features belong in profile bundles instead of new application bins; code that only adds plugins to an already-running app mounts those plugins directly.

Starting the app

You give your entry point a config file, and the process starts the whole app: it loads your environment layers, applies patches and profiles, boots every plugin, and returns once the app is running. In replay mode it boots the sibling cordis.snapshot.yml instead, so a recorded session reproduces identically. The smallest entry point is two calls:

installFailLoud('dsh')
const ctx = await boot('dsh', resolveConfigPath(argv[2], process.env.DSH_SNAPSHOT))

Startup policy follows the bundle that introduced each row. Only external / runtime rows may fail with a warning while their successful siblings remain active. Built-in, boot-staged, and unowned rows are required: an enabled failure disposes the app and rejects startup. Disabled rows are ignored; the bootstrap Include is always required. Direct calls without profile provenance are strict.

Profiles

Import profile and bundle declaration types from @deepseek-ai/dsh-package-manifest. App-boot adapts DshPackageManifest to ProfileManifest with optional package identity because local profiles need no published version. App-boot owns profile loading, JSON validation, and resolved runtime data.

A profile is how one dsh installation ships different app surfaces: web, headless, acp, sdk, and sdk-minimal start distinct compositions from the same launcher. A profile lives at $DSH_HOME/profiles/<name> and combines installable bundles, its own cordis.patch.yml, and patchReload: live | startup; omitted reload policy keeps the historical live default for custom profiles. The shipped web template uses live reload, while the other shipped templates apply patches only at startup. sdk-minimal names only its standalone bundle; the other templates retain base-plus-mode stacks. dsh --profile <name> --from-default-profile <template> creates a custom profile at a new non-shipped name from one shipped template, while dsh plugin initializes a base-backed profile and manages its installed bundles. A missing bundle or one without a patch declaration fails startup loudly. Application-owned npm projects, such as Electron's reserved Desktop profile, use loadProfileDirectory to load an already initialized directory without exposing it through CLI profile lookup.

Your machine-local preferences also live in the Harness home:

  • .env — your ordinary environment layers: the invoking directory's file outranks the Harness-home file, and both sit below the inherited environment. Variables that decide how the process starts (PATH, DSH_*, XDG_* and similar) are rejected from files: export them instead. The four proxy names (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY) are accepted from the Harness-home file only, never from the invoking directory's, which arrives with a clone. For a non-product bin that just wants one directory's .env, a missing file is fine and an unloadable one prints one labelled warning line.
  • cordis.patch.yml — your tweak layer, applied after every bundle layer (per-profile first, then the home-level file, which therefore outranks it): replace one entry's whole config (restating the fields you keep), insert new entries, or interpolate !!js expressions at boot. A patch naming an entry that does not exist prints a stderr warning; an empty or comments-only file fails boot — disable the layer with [] instead.

Profiles with patchReload: live watch both user patch files and apply the reload failure policy. A startup profile installs neither those watchers nor the launcher's watch-only HMR fallback.

Inserted plugin names may be absolute filesystem paths, file URLs, or package specifiers. Patch loading converts absolute paths and patch-relative ./ or ../ paths to file URLs within insert rows and their nested groups; existing-entry name assertions and replacement config values remain literal.

Installed dependencies default to external; template bundles and packages in dsh.profile.firstParty are builtin. Effective stage is dsh.profile.stages[package], then the package’s dsh.bundle.stage, then runtime. Stage selects startup failure policy, not a later execution phase. Bundle patches retain their declared ids, parents, and ordering. Built-in and boot-staged layers claim ids first; an optional bundle with a duplicate id is omitted whole and reported, while a conflicting user insert is omitted per row. dependencies records installation; dsh.profile.bundles selects enabled layers, including all their inserts and overrides.

The launcher provides ctx.profileRuntime before any configuration entry mounts. It owns row provenance, accepted composition, conflicts, and user-disabled rows. Watchers and management operations share its serial recomposition queue. Recomposition waits for current entries and removed fibers, then publishes accepted options and reports per-entry issues; a failed update can leave a fiber running its previous valid config. installFailLoud remains installed until shutdown for process-level unhandled rejections. Observers can await whenIdle() before publishing a refreshed view of composition ownership.

Patch files

Every layer above is a cordis.patch.yml: a top-level YAML sequence of the include plugin's PatchOptions — id-targeted overrides and insert lists — in the Loader's dialect, where !!js marks an expression the row's fiber evaluates. The ./patch-file export is the one place that reads and writes such a file, so a file the boot accepts is a file the agent-preset roster and the plugin manager accept.

Read a layer with parsePatchList (text in hand) or readPatchListFile (an 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:

import { mutatePatchFile } from '@deepseek-ai/dsh-app-boot/patch-file'

const file = '/home/me/.dsh/profiles/web/cordis.patch.yml'
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.

Previewing the effective configuration

Before you boot, you can print the exact configuration the app will mount: the dump shows the composed entry list with !!js expressions verbatim, grouped under comments naming each source file and the patch layers that changed it, as one loadable YAML document. Patches that match no row are reported with their layer label; a missing, unparsable, or invalid config fails the dump.

Startup and reload failures

After the Loader settles, app-boot reports optional failures as warnings and rejects startup if an enabled required entry cannot activate. In the table, stopping startup means disposing any mounted plugins and exiting nonzero without reporting readiness; continuing keeps successful plugins running. Later configuration HMR does not repeat the required-startup audit and does not roll back the whole update.

Failure pattern Optional entry at startup Required entry at startup Later configuration HMR
Root config or required overlay is missing, unreadable, malformed, or contains invalid entries Stop startup Stop startup Malformed or invalid live patches are rejected without changing the running configuration; a valid edit applies
Module import fails or module evaluation throws Warn; continue Stop startup Report the error; keep successful siblings; a corrected import can activate
Plugin config schema validation fails Warn; continue Stop startup A new entry stays inactive; an existing entry retains its prior instance and config; a valid correction applies
Config !!js evaluation throws Warn; continue Stop startup Report the error; keep successful siblings; a valid correction can activate
disabled: !!js evaluation throws Warn; continue Stop startup Report the evaluation error rather than treating the entry as disabled; a valid correction can activate
Synchronous apply() throws Warn; continue Stop startup Report the error; keep successful siblings; corrected config can activate
Asynchronous apply() throws Warn after settlement; continue Stop startup after settlement Report the error after settlement; keep successful siblings; corrected config can activate
An injected service is unavailable Warn; continue while the entry waits for its dependencies Stop startup Keep the entry waiting; adding the missing provider can activate it
HTTP port binding fails Warn; continue without that endpoint Stop startup Keep the process running without the failed endpoint; corrected config can restore it
Detached asynchronous work outside the apply() return Promise produces an unhandled rejection Fatal: dispose the app and exit nonzero Fatal: dispose the app and exit nonzero Fatal: dispose the app and exit nonzero, regardless of entry id
Entry is absent or explicitly disabled Ignore it Ignore it Do not activate it; no required-startup audit

An optional provider failure can leave a required consumer pending and prevent readiness. Overriding an existing row does not change its owner or startup policy. Schema rejection before an existing entry updates does not roll back successful sibling changes.

The Web process matrix and startup acceptance verify these outcomes through the shipped Web profile; app-boot tests also exercise root Include failures.

If your app owns the terminal, it can hand the terminal back before the process exits, so your shell is never left in raw mode. The handoff is bounded: a stuck cleanup delays the fatal exit but never cancels it.

Telling the agent where the harness lives

When your app boots a model-backed agent, you can tell the agent where the DSH implementation checkout lives: it learns that path and that it must not infer the working directory from it — it should use pwd. The instruction appears once near the top of the system prompt. Apps without a system prompt service skip it; in development, reloading the system prompt drops it until the next boot.


Understand the implementation

Implementation internals — click to expand This section explains how the outcomes above are realized and points at the code that realizes them; everything here is developer-facing and not needed to use the package. ### Design notes - **Channel-neutral library.** The package carries no loader hooks and no dev-mode surface; the [`dsh` app](../../../apps/cli/README.md) owns its Node source-launch hook and consumes these helpers for the boot sequence, and built consumers use plain Node package resolution. - **Two Loader builtins.** `mountRootInclude` registers `cordis:include` and `cordis:group` as Loader builtins: a group row gives one `isolate` realm to a provider and its consumers together, and an agent preset outside this workspace cannot resolve `@deepseek-ai/cordis-plugin-group` by name. Both load through the ambient module pipeline rather than the included tree's own specifier resolution. - **Consumer-owned strictness.** Ordinary Loader groups keep successful siblings. App-boot audits provenance after startup settlement; agent presets own and clean up generations that require every configured entry to activate. Entry failures are read from the Loader and Fiber, with duplicate rejection notifications coalesced through one process checkpoint. - **Profile module fallback.** Bare plugin specifiers resolve through the Loader from the config directory. Plain Node maintains one symlink per package in the installation dependency closure. A packaged executable instead reads each installed export map with Node ESM conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Missing exports stay unavailable, malformed maps fail startup, and a cross-process writer lock replaces stale entries without exposing partial proxies. A selected external bundle absent from the installation closure receives a profile-local `.dsh-module-fallback` link; existing pnpm entries win, projected links are excluded from later closure discovery, and cleanup removes only dsh-owned links. - **Update completion.** Live reload waits for Loader work before inspecting entry issues. Profile recomposition also waits for removed fibers, whose cleanup is absent from the current Loader tree. `Fiber.update()` and `Entry.update()` alone do not establish restart success. - **Two-stage failure labels.** `boot()` distinguishes `host preparation failed` — `prepare` threw before any config-tree entry mounted — from `plugin tree failed to load`. Plugin diagnostics include original stacks, nested causes, and aggregate member failures. Cyclic causes terminate diagnostic traversal without replacing the original cause. ### Helper behavior The exports each own one stage of the boot: config resolution and snapshot replay, layered environment loading, fail-loud reporting, activation auditing, patch parsing, root-include mounting, config dump rendering, live patch watching, profile composition, and the harness-source section. Per-export contracts live in the code, not this README — see [`src/index.ts`](src/index.ts) and [`src/profile.ts`](src/profile.ts). ### Source map | File | Role | |---|---| | [`src/index.ts`](src/index.ts) | Boot, environment layers, fail-loud guard, startup audit, patch parsing and watching, config dump | | [`src/profile.ts`](src/profile.ts) | Profile discovery, initialization, bundle resolution with trust and stage, module fallback | | [`src/external-bundles.ts`](src/external-bundles.ts) | Bundle ownership analysis and installed/enabled manifest lists | | [`src/compose-stack.ts`](src/compose-stack.ts) | Row-id ownership across the stack: `claimLayerIds`, `composeProfileStack`, conflict records | | [`src/entry-issues.ts`](src/entry-issues.ts) | Current entry failures, unresolved services, and diagnostic formatting | | [`src/profile-runtime.ts`](src/profile-runtime.ts) | The `profileRuntime` service: the committed composition (profile, row provenance, conflicts), user-disabled rows, recomposition | | [`src/package-metadata.ts`](src/package-metadata.ts) | Static manifest and patch declarations; no module execution or probe cache | | [`src/patch-file.ts`](src/patch-file.ts) | Public patch-file parser and atomic user-layer editor | | — | No runtime invariant companion is published; this presentation adapter owns no durable package-local event stream; boundary and replay tests cover its protocol mapping. |

Further Exploration

Read these pages when the package-level contract is not enough. They move from the shared boot mechanics to the composition model and the decision evidence behind it.


Model Experience

Indirectly, through the loaded plugin tree, which alone contributes model context; the one export that adds model-visible text, addHarnessSourceSection, does so only when a consumer calls it after boot.

KV Cache effect

Boot itself changes no request prefix. addHarnessSourceSection places its source path after first-party reusable instructions, so different checkouts leave those preceding bytes unchanged when tools and configuration match. Provider cache reuse is not guaranteed.

Known Limitations and Deferred Work

These limits describe when this boot library is a poor fit or needs special care. They are current package constraints, not a task backlog.

  • Bare package specifiers depend on Loader internals — production bins need Loader's optional native helper; an in-process caller without it must use resolvable relative/file specifiers or provide its own module-resolution hook.
  • Snapshot replay swapping is basename-specific — only a config ending in cordis.yml or cordis.yaml maps to the sibling cordis.snapshot.yml; custom config names require caller-managed selection.
  • Environment discovery is launch-scoped — loadLayeredEnv reads only the invocation directory and Harness home once; it does not search parents or follow a workspace selected later. loadEnv remains the one-directory helper for non-product bins.
  • A user patch replaces the whole matched config — an id-targeted patch does not deep-merge, so a profile override restates the bundle fields it keeps.
  • Overrides retain the target’s owner — changing a built-in row can make that required row fail; successful updates to other rows remain applied. Disabling the bundle removes its whole patch layer, including overrides.
  • A conflict is decided by order, not merit — among external bundles the earlier layer in dsh.profile.bundles keeps a contested id, so uninstalling that bundle lets the later one mount on the next boot; the plugin list shows which bundle lost and to whom.
  • The nested-fiber audit is advisory — a failed ctx.inject() continuation under a built-in entry is reported, not fatal, until shipped compositions are known clean.
  • Declarations are not activation results — readPackageMetadata reads dsh.plugins without importing modules. Declare plugins: [{ name: "." }] for a main export, or subpaths such as ./tools; undeclared packages remain unknown. Config validation and execution diagnostics appear only after an actual mount. Physical Cordis resolution cannot detect an inlined copy.

Dev Note

Working context for maintainers — click to expand This Dev Note is working context for maintainers: open design questions and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked Agent Notes. #### Open: config dump stability `renderConfigDump` output is a loadable YAML document whose `# ==` provenance comments and `!!js`-verbatim rendering serve the `--dump-config` diagnostic. Nothing promises byte stability across package versions; decide whether the dump becomes a serialization contract before anything consumes it programmatically.