@qlzt/dsh-app-boot 0.0.0-stage → 0.2.1-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,73 @@
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
3
+ # After editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
5
+ /:
6
+ en: 822ef8977098c703
7
+ zh: c0673e854310840f
8
+ /deepseek-ai-dsh-app-boot:
9
+ en: fdb73259898590a5
10
+ zh: aee057cd436da2e2
11
+ /deepseek-ai-dsh-app-boot/summary:
12
+ en: e5db70f7cdcdbe9c
13
+ zh: 6852efb61109a349
14
+ /deepseek-ai-dsh-app-boot/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 09388d293f9be9cb
17
+ /deepseek-ai-dsh-app-boot/use-this-package:
18
+ en: f6fa378225009c8f
19
+ zh: 324c0e4640bb1358
20
+ /deepseek-ai-dsh-app-boot/use-this-package/when-to-use-it:
21
+ en: 2fdfe02f2ed4a258
22
+ zh: d748228ece579c4a
23
+ /deepseek-ai-dsh-app-boot/use-this-package/starting-the-app:
24
+ en: 4a424cd401556f20
25
+ zh: 0b34dd18e0db6cea
26
+ /deepseek-ai-dsh-app-boot/use-this-package/profiles:
27
+ en: d50b0fdaf62353dd
28
+ zh: 0fccb6968602e5de
29
+ /deepseek-ai-dsh-app-boot/use-this-package/previewing-the-effective-configuration:
30
+ en: 646a0ccdd7881ad1
31
+ zh: e3696acb1573dd7b
32
+ /deepseek-ai-dsh-app-boot/use-this-package/inspecting-plugin-configuration-schemas:
33
+ en: c4a301652b851ce9
34
+ zh: 784871e19cd9db0a
35
+ /deepseek-ai-dsh-app-boot/use-this-package/reading-plugin-display-metadata:
36
+ en: a9afb263b03d112c
37
+ zh: 31f5e797cd953c15
38
+ /deepseek-ai-dsh-app-boot/use-this-package/startup-and-reload-failures:
39
+ en: 09890e218ab49c84
40
+ zh: e3d86597e7fb1adb
41
+ /deepseek-ai-dsh-app-boot/use-this-package/telling-the-agent-where-the-harness-lives:
42
+ en: 07c459f26f4f8517
43
+ zh: 210f1e4894731b1a
44
+ /deepseek-ai-dsh-app-boot/understand-the-implementation:
45
+ en: f4a2695cb4c8a570
46
+ zh: 89837235d2f376cf
47
+ /deepseek-ai-dsh-app-boot/understand-the-implementation/design-notes:
48
+ en: 25a0d1b46fd66d68
49
+ zh: 5cd8b4ac382dccdc
50
+ /deepseek-ai-dsh-app-boot/understand-the-implementation/helper-behavior:
51
+ en: f7bc3a8dd4b00bdd
52
+ zh: 30098428aa769ad0
53
+ /deepseek-ai-dsh-app-boot/understand-the-implementation/source-map:
54
+ en: 3da01827e3354655
55
+ zh: 06ccfaed83f65a76
56
+ /deepseek-ai-dsh-app-boot/further-exploration:
57
+ en: 401269cc5ea8593e
58
+ zh: cd337c356b320565
59
+ /deepseek-ai-dsh-app-boot/model-experience:
60
+ en: 769777f1cb1a9408
61
+ zh: f02b2834179df50d
62
+ /deepseek-ai-dsh-app-boot/model-experience/kv-cache-effect:
63
+ en: c4d49630b68ae896
64
+ zh: 6f450ec6ba39bb81
65
+ /deepseek-ai-dsh-app-boot/known-limitations-and-deferred-work:
66
+ en: bf1a49013cad8e9c
67
+ zh: 41b2b010f8650ec3
68
+ /deepseek-ai-dsh-app-boot/known-limitations-and-deferred-work/dev-note:
69
+ en: d2f0d68fe29fb750
70
+ zh: 323d21ee73a9e3f6
71
+ /deepseek-ai-dsh-app-boot/known-limitations-and-deferred-work/dev-note/open-yaml-config-dump-stability:
72
+ en: 032f14ed5cd78efa
73
+ zh: ecc81f819f0bc39b
package/README.md CHANGED
@@ -1,3 +1,222 @@
1
- # Temporary Holding Version
1
+ ---
2
+ description: "Shared Loader boot support for dsh profiles and the temporary Python SDK runtime: environment layers, patches, diagnostics, and configuration preview."
3
+ kind: "package-library"
4
+ ---
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # @deepseek-ai/dsh-app-boot
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ `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, configure HMR through profile YAML, and let a terminal-owning app restore its terminal before a fatal exit.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ 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.
29
+
30
+ ### When to use it
31
+
32
+ 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.
33
+
34
+ ### Starting the app
35
+
36
+ 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:
37
+
38
+ ```text
39
+ installFailLoud('dsh')
40
+ const ctx = await boot('dsh', resolveConfigPath(argv[2], process.env.DSH_SNAPSHOT))
41
+ ```
42
+
43
+ `installFailLoud` writes one labelled `util.inspect` diagnostic to stderr for an unhandled rejection or an uncaught exception, awaits the surface's release hook under a fixed timeout, and exits 1; control never returns to the failed operation, because only the throw site knows which state is intact, and the event loop runs only until the release settles or times out. With that entry point, startup keeps every plugin that can activate. An enabled failed plugin produces a labelled warning. A failed required entry makes startup dispose the whole app and exit nonzero; required ids absent from a profile and disabled required entries do not affect startup. The global required list covers shared Agent execution, application endpoints, and Web bootstrap/transport: `agent-loop`, `webserver`, `web-runtime`, `modules`, `connection`, `headless-runner`, `acp`, and `sdk-jsonrpc-server`.
44
+
45
+ <a id="profiles"></a>
46
+ ### Profiles
47
+
48
+ Import profile and bundle declaration types from [`@deepseek-ai/dsh-package-manifest`](../../util/package-manifest/README.md). 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.
49
+
50
+ 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 with its own `cordis.patch.yml`. A bundle's `dsh.bundle.patch` names one patch file or an ordered list of files; `bundlePatchFiles` validates the declaration and `bundlePatchPaths` resolves it to absolute paths; the layer concatenates their patch lists in that order. The YAML composition enables or disables HMR. 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. Bundle resolution, manifest, and patch-loading failures skip that bundle without changing its selection; the loaded profile lists each skip in `skippedBundles`, and the launcher prints them once per start with `reportSkippedBundles`. Remaining bundles keep their order; profile and user-patch errors still fail startup. Skipping a bundle does not guarantee that the remaining composition can provide the required services. 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.
51
+
52
+ Before a profile imports a plugin, DSH checks its `peerDependencies` on `@deepseek-ai/dsh` and `@deepseek-ai/dsh-*` against the single runtime version returned by `getDshRuntimeVersion()`. Every declared range must match; prereleases participate in range matching. Source-workspace `workspace:^`, `workspace:~`, and `workspace:*` refer to that same runtime. Missing DSH peers impose no constraint; invalid ranges are incompatible. These checks use peer declarations, not `engines.dsh`, and are not a sandbox against malicious package code.
53
+
54
+ Admission happens only at composition boundaries DSH owns, and a denied row is denied in the launcher's own copy of the composition: no profile patch layer, dependency manifest, or bundle list changes. `prepareProfilePatches` composes over the launcher's empty profile root and runs while the root Include is mounted and on every profile recomposition, so a denied plugin never imports its module. `prepareProfileEntries` does the same for a preset's rows. A denied ordinary row becomes a detached `disabled: true` row; a native group stays mounted and its denied children do not load; a native Include that reaches a denied plugin is omitted whole, because its file is never rewritten. A row denied by policy keeps its configured `disabled` value in the profile, and every refusal is reported with the package, versions, and risk. A bundle is not a row, so `loadProfileDirectory` checks each bundle's own DSH peers when it loads the profile's bundle layers, at startup and on every recomposition; an incompatible bundle without an exemption is skipped like an unreadable one and listed in `skippedBundles`. These boundaries do not cover plugins another embedder mounts through its own `ctx.plugin` call. Two edits made directly to files during a session are judged only at the next recomposition or startup: a running plugin's own `package.json` peer declaration, and an entry-list file the Loader reads itself, such as the launcher's root config or a nested `cordis:include` file. `--dump-config` reports the configured composition, so a denied plugin row still appears there while a denied bundle contributes no rows, and `--dump-config-schema` imports each composed module to read its schema; run it only against a profile whose plugins you already trust.
55
+
56
+ Exact-version exemptions live in the profile's own `compatibility.json`, not in its `package.json`, so an exemption write never touches the dependency manifest, the bundle list, or Cordis patch files. It maps exact `package-name@version` keys to lists of exact DSH runtime versions; neither a plugin upgrade nor a DSH upgrade inherits permission. A missing file means no exemptions. A malformed one never stops the profile from starting: records the reader accepts still apply, each rejected record is reported on stderr beside the plugin denials, and the file is then treated as read-only, so a grant or revocation refuses and asks the user to repair it by hand instead of overwriting their content. [Plugin Manager](../plugin-manager/README.md#version-compatibility-and-exemptions) owns the grant, revocation, and risk confirmation each change requires.
57
+
58
+ Your machine-local preferences also live in the Harness home:
59
+
60
+ - **`.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.
61
+ - **`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.
62
+
63
+ The enabled `dsh-hmr` plugin watches the profile manifest and both user patch files, re-reads the ordered bundle layers, and applies the [reload failure policy](#startup-and-reload-failures). [DSH HMR](../hmr/README.md) serializes these reloads with [Plugin Manager](../plugin-manager/README.md) configuration writes; package operations run outside its queue. The launcher does not install HMR or watchers; disabled or absent HMR means changes require restart.
64
+
65
+ 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.
66
+
67
+ Before mounting profile rows, the `dsh` launcher computes one immutable runtime resolution from the installation and ordered bundle dependency graphs. Every profile launcher uses runtime resolution, including plain Node, packaged executables, and the Electron Host. It installs the runtime resolution through Node's ESM and CommonJS resolvers without creating fallback links.
68
+
69
+ `sanitizeProfile(binName, profileDir, bundles)` provides filesystem recovery without loading plugins or parsing patches. Desktop uses it for native fatal recovery. Call it only after stopping the profile and excluding concurrent profile writes. It renames the profile’s `cordis.patch.yml` to a unique `.bak-<timestamp>` sibling and restores the supplied bundle list, preserving installed packages and other manifest fields. The timestamp is Unix time in milliseconds; collisions append an ordinal (`-1`, `-2`, …) without changing it. It returns the backup path, or `undefined` when no patch exists; missing profiles remain absent. Profile initialization recreates an empty patch on the next launch. The home-level patch is unchanged. Invalid profile JSON fails before mutation; later errors propagate and retain completed changes for retry.
70
+
71
+ ### Previewing the effective configuration
72
+
73
+ 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.
74
+
75
+ ### Inspecting plugin configuration schemas
76
+
77
+ `generateConfigSchema` takes a diagnostic bin name, a prepared on-disk profile, ordered patch lists, and an installation anchor, and returns `ConfigSchemaDump`. App-boot owns composition, runtime resolution, and collection diagnostics. The caller owns profile preparation, home/argv layer selection, process streams, and exit policy. `createConfigProjector`, `isNativeConfigSchema`, and `LOADER_EXPRESSION_SCHEMA` are exported for callers that project one live plugin Config without profile collection; projected value positions reference `#/$defs/loaderExpression`, so the enclosing document must define it.
78
+
79
+ The generated JSON Schema 2020-12 describes the composed entry list, with `$defs.patchList` for root-tree overlays and shared definitions projected from plugin Config graphs. It includes disabled entries, native groups, and literal YAML/JSON includes; builtin and canonical native package exports are matched using each tree's module-resolution base, including profile-local copies. Custom carriers are not inferred from their config fields. A missing include with literal `initial` entries is expanded in memory without writes. Discovery and projection diagnostics remain in `x-cordis`, including unknown Configs and partial constraints. The [CLI schema-dump reference](../../../apps/cli/reference/README.md#config-schema-dump) owns the output fields and editing semantics.
80
+
81
+ The projector preserves native omission behavior by checking literal defaults against generated schemas with Ajv, without executing native validators or transform callbacks. Regex compatibility checks and unsupported or recursive-default cases produce explicit limitations. Opaque input adaptations and lazy metadata effects widen validation rather than replaying native mutation. Non-JSON default/presentation annotations are omitted with limitations without losing the structural schema; an unrepresentable default leaves omission acceptance unknown unless the field is required. These dependencies load only when collection runs. Imports, Config getters, and lazy builders still execute trusted code; collection is not a sandbox. Do not overlap profile-resolution interceptions. The collector releases its interception before returning, while Node retains imported modules; runtime-created plugins and Agent preset instances remain outside discovery.
82
+
83
+ ### Reading plugin display metadata
84
+
85
+ `resolvePluginResource(specifier, parentURL)` resolves a plugin module or exported resource to its local filesystem path through the active Node ESM resolver without evaluating it. It throws when the resolver is unavailable or the resource cannot resolve to a local file.
86
+
87
+ Use `readPluginMeta(specifier, parentURL)` or `ctx.pluginPackages.metaOf(specifier, parentURL)` to read installed package display text without importing or activating the plugin. Lookup uses the complete package specifier and the caller's resolution base, respecting Node exports. File paths and file URLs return no metadata without resolving resources. For a package-root specifier, missing locale fields fall back to the accessible `package.json`; a subpath specifier never reads a `package.json`. Malformed metadata returns an `error` diagnostic. Results retain translations for Client-side language selection. The reader also loads an image data URL, even when locale text is complete: a package root uses its manifest `icon`, or `<package>/icon` when that field is omitted; a subpath uses `<specifier>/icon`. An icon error preserves valid text alongside the diagnostic. See [Plugin display metadata](../../../docs/cookbook/adding-a-package.md#plugin-display-metadata) for the author format.
88
+
89
+ `ON_DEMAND_BUNDLES` names the public Official packages installed only on request. `OFFICIAL_ON_DEMAND_CATALOG` embeds their package-owned localized metadata and icons for offline discovery; `pnpm gen-official-bundle-catalog` explicitly updates generated content, and `verify-official-bundle-catalog` checks resource completeness and freshness; builds consume the committed catalog without importing provider code. The catalog contains no install target: [Plugin Manager](../plugin-manager/README.md#use-this-package) derives it from the running version and installation and uses the ordinary bundle installer. The [product-use check](../../../scripts/verify-product-use.ts) composes shipped and on-demand selections against Web without executing providers.
90
+
91
+ <a id="startup-and-reload-failures"></a>
92
+ ### Startup and reload failures
93
+
94
+ Profile reconciliation returns diagnostics for unchanged inactive entries without failing an unrelated mutation. A new inactive entry, a changed configuration or fiber, or a changed diagnostic fails reconciliation; removed fibers must still finish disposal. Explicit enablement targets must activate even when their failure predates the operation. Successful reconciliation emits `app-boot/config-reload` after lifecycle settlement and diagnostic checks, including programmatic updates without HMR. The event carries no diff or parsed config. Successful reconciliation returns after lifecycle settlement and diagnostic checks; volatile-only entry changes are committed by Loader during the update.
95
+
96
+ After the Loader settles, app-boot warns when only optional entries are inactive. If an enabled required entry cannot activate, `boot()` rejects with `StartupError` after disposal. An independently owned logger exporter retains warning and error records through asynchronous disposal and is released before `boot()` settles. Its message groups all failed plugins and pending services, marks required entries, and retains original stacks, nested causes, and aggregate members. The CLI prints that message once and saves [full startup diagnostics](../../../apps/cli/reference/README.md#startup-diagnostics) before exiting with code 1; unrelated exceptions retain their normal stack output. 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.
97
+
98
+ | Failure pattern | Optional entry at startup | Required entry at startup | Later configuration HMR |
99
+ |---|---|---|---|
100
+ | 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 |
101
+ | Module import fails or module evaluation throws | Warn; continue | Stop startup | Report the error; keep successful siblings; a corrected import can activate |
102
+ | 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 |
103
+ | Config `!!js` evaluation throws | Warn; continue | Stop startup | Report the error; keep successful siblings; a valid correction can activate |
104
+ | `disabled: !!js` evaluation throws | Warn; continue | Stop startup | Report the evaluation error rather than treating the entry as disabled; a valid correction can activate |
105
+ | Synchronous `apply()` throws | Warn; continue | Stop startup | Report the error; keep successful siblings; corrected config can activate |
106
+ | Asynchronous `apply()` throws | Warn after settlement; continue | Stop startup after settlement | Report the error after settlement; keep successful siblings; corrected config can activate |
107
+ | 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 |
108
+ | HTTP port binding fails | Warn; continue without that endpoint | Stop startup | Keep the process running without the failed endpoint; corrected config can restore it |
109
+ | 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 |
110
+ | A synchronous callback (a stream `'data'` listener, a timer) throws an uncaught exception at any point in the process lifetime | Fatal: dispose the app and exit nonzero; the failed operation is not resumed | Fatal: dispose the app and exit nonzero; the failed operation is not resumed | Fatal: dispose the app and exit nonzero, regardless of entry id |
111
+ | Entry is absent or explicitly disabled | Ignore it | Ignore it | Do not activate it; no required-startup audit |
112
+
113
+ The required list above includes `modules` and `connection`; Web startup cannot succeed when either enabled entry fails. Failure of an optional provider can also prevent a required consumer from activating. Schema rejection before an existing entry updates is not a transactional rollback of sibling changes.
114
+
115
+ The [Web process matrix](../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts) and [startup acceptance](../../../apps/cli/tests/profiles/web/tests/web-best-effort-startup.expected.e2e.ts) verify these outcomes through the shipped Web profile; [app-boot tests](tests/app-boot.spec.ts) also exercise root Include failures.
116
+
117
+ 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.
118
+
119
+ ### Telling the agent where the harness lives
120
+
121
+ 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.
122
+
123
+ -----
124
+
125
+ <a id="understand-the-implementation"></a>
126
+ ## Understand the implementation
127
+
128
+ <details>
129
+ <summary>Implementation internals — click to expand</summary>
130
+
131
+ 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.
132
+
133
+ ### Design notes
134
+
135
+ - **Runtime version.** `getDshRuntimeVersion()` reads this package's manifest through a filesystem path, including the executable's virtual filesystem; missing or invalid versions fail rather than bypassing compatibility checks.
136
+ - **Profile launch data.** `ctx.profileContext` contains only profile locations, startup bundle names, parsed invocation overlays and the telemetry opt-out value. `readProfilePatches()` composes the supplied startup profile or reads current files at those locations; callers schedule and apply the result.
137
+ - **Process-local module resolution.** The launcher installs the runtime resolution on Node's internal ESM and CommonJS resolvers before profile rows mount. Node still owns exports, conditions, subpaths, module caches, and error codes; routed ESM failures report the original importer. Explicit CommonJS `paths` always retain native lookup, including paths inside profiles.
138
+ - **Linked directories.** A profile link to an external directory admits its importers to peer-aware ancestor lookup, even without its own `package.json`. At each `D/node_modules` position, current `D/package.json` peer names present in the runtime table use the runtime package; other names use the physical candidate. A nearer physical package precedes a later peer declaration, and a peer position needs no physical `node_modules`. Installation-scope package directories are excluded from linked interception; overlapping roots do not change the importer's lookup order ([rule](../../../.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.md)).
139
+ - **Package metadata.** `ctx.pluginPackages.packageOf` locates the owning package without loading code or requiring an exported `package.json`; a subpath selects its package without validating that file. The installed runtime resolution owns selection even for a miss. Low-level embedders that install the service without one retain native lookup. Display metadata uses the separate entry-aware reader described above.
140
+ - **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.
141
+ - **Consumer-owned strictness.** Ordinary Loader groups keep successful siblings. App-boot applies the global required-entry policy after initial settlement; agent presets and dynamic multi-entry compositions own and dispose their separate Loader subtree when they require all-or-nothing setup. App-boot reads failed fibers to report their recorded errors and coalesces duplicate Loader rejection notifications through one process checkpoint.
142
+ - **One runtime resolution.** The installation-first and ordered-bundle breadth-first traversal produces the runtime table. Runtime resolution creates no links; runtime resolution entries occupy their package names at `$DSH_HOME/profiles/node_modules`, and every other name sees that directory as an ordinary ancestor. Profile load removes the `.dsh-module-fallback` projections that link-backend releases wrote into a profile; pnpm-installed packages stay. External bare targets selected by package `imports` use the same package order, while Node retains mapping, conditions, and exact target resolution. A complete successor may add package names, remove profile-scoped mappings and profile-local names, and update linked-root membership atomically; retained mappings keep their normalized directory, version, and scope. Installation anchors remain fixed; a profile mapping may change its declarer. [Plugin Manager](../plugin-manager/README.md) publishes successors after package operations ([decision](../../../.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md)).
143
+ - **Removing linked interception.** A successor may remove a linked root without restart. Directories outside all remaining roots use native lookup for subsequent requests, which may find a development copy or report a missing package. Existing module references and Node caches stay unchanged. Re-adding the same link name and target is allowed; pointing a previously published name at a different target is rejected even after removal ([generation rules](../../../.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md#immutable-generations)).
144
+ - **Application-owned profiles.** Application-owned profiles use the same runtime resolution. Links within the active profile directory, including pnpm store links, are not external roots even when the profile is outside the shared profiles tree. Resolution does not modify their `node_modules`; pnpm owns installed packages.
145
+ - **Owned Workers.** Worker build banners import `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` before bundled business code. Each Worker installs the structured-cloned runtime resolution in its own isolate. The bootstrap bundle has no static package imports. Source Worker entries retain their self-contained dependency closure, and third-party Workers receive no injection.
146
+ - **Update completion.** App boot observes restart failures through the `internal/update` waterfall. Live patch reloads wait for the tree's fibers before auditing activation; `Fiber.update()` and `Entry.update()` alone do not establish restart success.
147
+ - **One rejection checkpoint.** `inactiveEntries` keeps the exact reasons it folds into the boot diagnostic visible through the next process rejection checkpoint, so `installFailLoud` coalesces Loader's duplicate notification while unrelated unhandled rejections remain fatal.
148
+ - **Two-stage failure labels.** Outside startup audit failures, `boot()` distinguishes `host preparation failed` — `prepare` threw before any config-tree entry mounted — from `plugin tree failed to load`, and appends the deepest plugin error's stack. Plugin diagnostics retain nested causes and aggregate member failures; cyclic causes stop traversal without replacing the original error.
149
+
150
+ The startup error also retains inactive-entry metadata and raw startup warning/error records without retaining the Loader tree. Its `entries` and `startup` fields are non-enumerable: direct access and full diagnostic reports retain them, while ordinary error inspection omits them. Pending-only failures have no `cause`; failures with recorded errors retain their original values in an `AggregateError`. Import errors are collected through the logger before the Loader mounts because no failed Fiber exists for those imports. The temporary exporter is removed when boot settles.
151
+
152
+ ### Helper behavior
153
+
154
+ 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, 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).
155
+
156
+ ### Source map
157
+
158
+ | File | Role |
159
+ |---|---|
160
+ | [`src/index.ts`](src/index.ts) | Boot helpers: config resolution, environment loading, fail-loud guard, activation audit, patch parsing, config dump, harness-source section |
161
+ | [`src/profile.ts`](src/profile.ts) | Profile discovery, initialization, bundle resolution, runtime resolution construction |
162
+ | [`src/profile-plugins.ts`](src/profile-plugins.ts) | Installed dependencies, bundle activation policy, and manifest updates |
163
+ | [`src/profile-sanitize.ts`](src/profile-sanitize.ts) | Profile patch backup and recovery bundle activation |
164
+ | [`src/config-schema/`](src/config-schema/) | Profile schema generation, discovery, native projection, and result types |
165
+ | [`src/profile-resolution/`](src/profile-resolution/) | Runtime resolver, package-metadata service, and built Worker bootstrap |
166
+
167
+ </details>
168
+
169
+ -----
170
+
171
+ <a id="further-exploration"></a>
172
+ ## Further Exploration
173
+
174
+ 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.
175
+
176
+ - [Cordis primer](../../../docs/cordis-primer.md) — Loader, `!!js` config expressions, and include/group semantics.
177
+ - [dsh app](../../../apps/cli/README.md) — the `dsh` bin that consumes these helpers.
178
+ - [dsh-cmdline](../cmdline/README.md) — the launcher-to-app command-line handoff the bins use.
179
+ - [Profile bundles](../../bundle/README.md) — installable patch layers composed into `dsh --profile`.
180
+ - [dsh-home-paths](../../util/home-paths/README.md) — the Harness-home resolver (`resolveDshHome`).
181
+ - [Configuration source ownership](../../../.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md) — why a discovered file may not decide bootstrap behavior.
182
+ - [Profile plugin bundles](../../../.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md) — the profile and bundle composition design.
183
+ - [User-patch HMR tests](tests/user-patches.spec.ts) — ownership of live-patch behavior and native filesystem delivery.
184
+
185
+ -----
186
+
187
+ <a id="model-experience"></a>
188
+ ## Model Experience
189
+
190
+ 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.
191
+
192
+ #### KV Cache effect
193
+
194
+ 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.
195
+
196
+ ## Known Limitations and Deferred Work
197
+
198
+ <a id="known-limitations-and-deferred-work"></a>
199
+
200
+ These limits describe when this boot library is a poor fit or needs special care. They are current package constraints, not a task backlog.
201
+
202
+ - **Runtime resolution depends on Node internals** — supported Node versions require the native builtin-access addon and executable compatibility coverage. Only built Harness-owned Workers receive the runtime resolution bootstrap; third-party Workers and custom `vm` linkers keep native resolution.
203
+ - **Relinking a profile package needs a restart** — Node caches real paths, so changing the target of a profile link or a dependency link requires a process restart.
204
+ - **Linked scope follows recorded real directories** — a hoisted dependency outside every linked root uses native Node. A peer read never invalidates Node caches or changes which files are watched.
205
+ - **Source launches use an ESM-only hook** — CommonJS requests still need the JavaScript files selected by package exports; resolution does not supply missing build outputs.
206
+ - **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.
207
+ - **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.
208
+ - **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.
209
+
210
+ <a id="dev-note"></a>
211
+ ### Dev Note
212
+
213
+ <details>
214
+ <summary>Working context for maintainers — click to expand</summary>
215
+
216
+ 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.
217
+
218
+ #### Open: YAML config dump stability
219
+
220
+ `renderConfigDump` output is a loadable YAML document whose `# ==` source 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. JSON Schema output follows the separate [pre-stable compatibility policy](../../../apps/cli/reference/README.md#config-schema-dump).
221
+
222
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,222 @@
1
+ ---
2
+ description: "dsh profile 与临时 Python SDK 运行时的共享 Loader 启动支持:环境层、patch、诊断与配置预览。"
3
+ kind: "package-library"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-app-boot
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ `dsh-app-boot` 是 `dsh` profile(包括 Python 运行时 wheel 包所含的 CLI(命令行界面))背后的共享 Loader 启动库。它加载环境层、组合 profile 组合包与 patch、启动每个插件,再返回运行中的应用,或指出失败插件与原因。产品应用使用 `dsh` launcher 而不发布单独 bin;直接配置 helper 只保留给低层嵌入方与测试。你还可以在启动前预览生效配置,按 profile 选择实时或仅启动时应用 patch,并让持有终端的应用在致命退出前恢复终端。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 用此包启动应用是一个小而显式的入口:你给它一个配置文件,它运行整个启动过程。本节说明你能做什么、能得到什么;每个结果背后的 helper 调用记录在下方可折叠的实现章节中。
29
+
30
+ ### 何时使用
31
+
32
+ 在实现共享 `dsh` launcher 或嵌入其低层启动 helper 时使用它。产品功能应放入 profile 组合包,而不是新增应用 bin;只向已运行应用添加插件的代码直接挂载插件即可。
33
+
34
+ ### 启动应用
35
+
36
+ 你把配置文件交给入口,进程就会启动整个应用:加载环境层、应用 patch 与 profile、启动每个插件,并在应用运行后返回。在回放模式下,它会启动同级的 `cordis.snapshot.yml` 替代文件,使已记录的会话能够原样复现。最小的入口只需两次调用:
37
+
38
+ ```text
39
+ installFailLoud('dsh')
40
+ const ctx = await boot('dsh', resolveConfigPath(argv[2], process.env.DSH_SNAPSHOT))
41
+ ```
42
+
43
+ `installFailLoud` 会为未处理 rejection 或未捕获异常向 stderr 写一条带标签的 `util.inspect` 诊断,在固定超时内等待界面的 release 钩子,然后以 1 退出;控制流不会回到失败的操作,因为只有抛出点知道哪些状态仍然完整,事件循环只运行到 release 结束或超时。有了这个入口,启动会保留所有能够激活的插件。启用但失败的插件会产生带标签的警告。required entry 失败时,启动会拆卸整个应用并以非零码退出;profile 中不存在的 required id 和已禁用的 required entry 不影响启动。全局 required list 覆盖共享 Agent 执行、应用 endpoint,以及 Web 启动与传输:`agent-loop`、`webserver`、`web-runtime`、`modules`、`connection`、`headless-runner`、`acp` 和 `sdk-jsonrpc-server`。
44
+
45
+ <a id="profiles"></a>
46
+ ### Profile
47
+
48
+ Profile 与组合包的声明类型从 [`@deepseek-ai/dsh-package-manifest`](../../util/package-manifest/README.zh.md) 导入。App-boot 将 `DshPackageManifest` 适配为包身份可选的 `ProfileManifest`,因为本地 profile 无需发布版本。App-boot 负责 profile 加载、JSON 校验和解析后的运行时数据。
49
+
50
+ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`headless`、`acp`、`sdk` 与 `sdk-minimal` 从同一 launcher 启动不同组合。profile 位于 `$DSH_HOME/profiles/<name>`,由可安装组合包和自身 `cordis.patch.yml` 组成。组合包的 `dsh.bundle.patch` 指定一个 patch 文件或一个有序的文件列表;`bundlePatchFiles` 校验该声明,`bundlePatchPaths` 把它解析为绝对路径;该层按此顺序拼接各文件的 patch 列表。YAML 组合决定是否启用 HMR。随产品交付的 `web` 模板实时重载,其他随附模板只在启动时应用 patch。`sdk-minimal` 只列出自身的独立组合包,其他模板保留 base 加模式的组合包栈。`dsh --profile <name> --from-default-profile <template>` 从一个随附模板,在新的非内置名称处创建自定义 profile;`dsh plugin` 则初始化以 base 为基础的 profile,并管理其中安装的组合包。组合包解析、manifest 读取或 patch 加载失败时会跳过该组合包,不改变其选择状态;加载结果在 `skippedBundles` 中列出每个被跳过的组合包,启动器每次启动时通过 `reportSkippedBundles` 输出一次。其余组合包保持原顺序;profile 和用户 patch 错误仍会导致启动失败。跳过组合包不保证剩余组合能够提供所需服务。由应用持有的 npm 项目(例如 Electron 保留的 Desktop profile)通过 `loadProfileDirectory` 加载已经初始化的目录,而不会将它暴露给 CLI profile 查找。
51
+
52
+ profile 导入插件前,DSH 会检查其 `peerDependencies` 中对 `@deepseek-ai/dsh` 和 `@deepseek-ai/dsh-*` 的依赖,与 `getDshRuntimeVersion()` 返回的唯一运行时版本比较。每个声明的版本范围都必须匹配;预发布版本参与范围匹配。源码工作区的 `workspace:^`、`workspace:~` 和 `workspace:*` 指向同一个运行时。未声明 DSH peer 时不施加版本约束;无效范围视为不兼容。这些检查使用 peer 声明,而不是 `engines.dsh`,也不是防范恶意包代码的沙箱。
53
+
54
+ 检查只发生在 DSH 自己持有的组合入口,改写的是启动器自己的那份组合:profile 的 patch 层、依赖清单与组合包列表都不会改变。`prepareProfilePatches` 在启动器的空 profile 根之上组合,并在挂载根 Include 时以及每次 profile 重新组合时执行,因此被拒绝的插件永远不会导入其模块;`prepareProfileEntries` 对 preset 行做同样的事。被拒绝的普通行会变成游离的 `disabled: true` 行;原生 group 保持挂载,其被拒绝的子行不会加载;若某个原生 Include 会到达被拒绝的插件,则整体省略该 Include,因为它的文件不会被改写。被策略拒绝的行在 profile 中保留其配置的 `disabled` 值,每次拒绝都会报告包名、版本与风险。组合包本身不是行,因此 `loadProfileDirectory` 在启动和每次重新组合加载 profile 的组合包层时,检查每个组合包自己声明的 DSH peer;没有豁免的不兼容组合包会像无法读取的组合包一样被跳过,并列入 `skippedBundles`。这些入口不覆盖其他嵌入方通过自己的 `ctx.plugin` 调用挂载的插件。会话中直接改文件的两类编辑只在下一次重新组合或启动时才被判定:正在运行的插件自己的 `package.json` peer 声明,以及 Loader 自己读取的入口清单文件(如启动器的根配置或嵌套的 `cordis:include` 文件)。`--dump-config` 报告的是配置出的组合,因此被拒绝的插件行仍会出现在其中,而被拒绝的组合包不提供任何行;`--dump-config-schema` 会导入每个组合模块以读取其 schema,请只对已经信任其插件的 profile 运行。
55
+
56
+ 精确版本豁免保存在 profile 自己的 `compatibility.json` 中,而不是 `package.json`,因此写豁免不会触及依赖清单、组合包列表或 Cordis patch 文件。它把精确的 `package-name@version` 键映射到精确 DSH 运行时版本列表;插件升级和 DSH 升级都不继承授权。文件缺失表示没有豁免。文件损坏绝不会阻止 profile 启动:读取器接受的记录仍然生效,每条被拒绝的记录会与插件拒绝信息一起输出到 stderr,此后该文件被视为只读,因此授予或撤销会拒绝执行并要求用户手工修复,而不是覆盖用户的内容。[插件管理器](../plugin-manager/README.zh.md#version-compatibility-and-exemptions)负责每次变更所需的授权、撤销与风险确认。
57
+
58
+ 你的机器本地偏好同样位于 harness home 中:
59
+
60
+ - **`.env`**——你的普通环境层:调用目录的文件优先于 harness home 的文件,两者都低于继承环境。在文件中设置的进程启动变量(如 `PATH`、`DSH_*`、`XDG_*`)会被拒绝:请改为导出这些变量。四个代理名(`HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY`、`NO_PROXY`)只从 harness home 的文件接受,绝不从调用目录的文件接受——后者随 clone 一起到来。对于只想加载某个目录 `.env` 的非产品 bin,文件缺失不影响启动,文件无法加载时输出一行带标签的警告。
61
+ - **`cordis.patch.yml`**——你的 tweak 层,应用在所有组合包层之后(先应用逐 profile 的文件,再应用 home 级文件,因此后者优先级更高):替换某个条目的整个配置(重述你要保留的字段)、插入新条目,或在启动时插值 `!!js` 表达式。patch 指定的条目不存在时输出 stderr 警告;空文件或仅含注释的文件会导致启动失败——如需禁用该层,请改用 `[]`。
62
+
63
+ 启用的 `dsh-hmr` 插件会监视 profile manifest 与两份用户 patch 文件,重新读取按顺序排列的组合包层,并应用[重载失败策略](#startup-and-reload-failures)。[DSH HMR](../hmr/README.zh.md) 将这些重载与[插件管理器](../plugin-manager/README.zh.md)的配置写入串行化;包操作在其队列之外执行。启动器不安装 HMR 或监视器;HMR 被禁用或不存在时,更改需要重启。
64
+
65
+ 插入条目的插件名可以是绝对文件系统路径、文件 URL 或包标识符。patch 加载会把 `insert` 条目及其嵌套分组中的绝对路径以及相对于 patch 文件的 `./` 或 `../` 路径转换为文件 URL;对已有条目名称的断言及替换用的 `config` 值保持原样。
66
+
67
+ 挂载 profile 条目前,`dsh` launcher 会从安装依赖图与有序 bundle 依赖图计算一份不可变的 runtime resolution。普通 Node、打包可执行文件与 Electron Host 等所有 profile 启动器都使用 runtime 解析,将 runtime resolution 安装到 Node 的 ESM 与 CommonJS 解析器中,不创建 fallback 链接。
68
+
69
+ `sanitizeProfile(binName, profileDir, bundles)` 提供文件恢复,无需加载插件或解析 patch。Desktop 在原生致命错误恢复中调用它。调用前必须停止 profile 并排除并发 profile 写入。它将 profile 的 `cordis.patch.yml` 重命名为带唯一 `.bak-<timestamp>` 后缀的同目录备份,并恢复调用方指定的 bundle 列表,保留已安装包和其他 manifest 字段。时间戳为 Unix 毫秒数;同名备份已存在时追加序号(`-1`、`-2`、……),时间戳保持不变。返回值为备份路径;patch 不存在时返回 `undefined`,缺失的 profile 不会被创建。下次启动的 profile 初始化会重新创建空 patch。home 级 patch 不变。无效 profile JSON 在修改前报错;后续错误向调用方抛出,保留已完成的修改供重试。
70
+
71
+ ### 预览生效配置
72
+
73
+ 启动前,你可以打印应用将挂载的确切配置:dump 会以 `!!js` 表达式原样展示组合后的条目列表,并按注释分组标明每个源文件及其 patch 层,输出是一份可加载的 YAML 文档。未匹配到任何行的 patch 会连同其层标签一起报告;配置缺失、无法解析或字段无效都会使 dump 失败。
74
+
75
+ ### 检查插件配置 schema
76
+
77
+ `generateConfigSchema` 接收用于诊断的 bin 名称、已准备好的磁盘 profile、有序 patch 列表和安装锚点,返回 `ConfigSchemaDump`。App-boot 负责组合、运行时解析和收集诊断。调用方负责 profile 准备、home/argv 层选择、进程流及退出策略。`createConfigProjector`、`isNativeConfigSchema` 与 `LOADER_EXPRESSION_SCHEMA` 供不经 profile 收集、只投影单个运行中插件 Config 的调用方使用;投影后的取值位置会引用 `#/$defs/loaderExpression`,外层文档必须定义它。
78
+
79
+ 生成的 JSON Schema 2020-12 描述组合后的 entry list,以 `$defs.patchList` 描述根树 overlay,并从插件 Config 图投影共享定义。它包含禁用项、原生 group 和字面量 YAML/JSON include;内置与规范原生包导出按每棵树的模块解析基准匹配,包括 profile 本地副本。不会根据 config 字段猜测自定义承载插件。include 缺失但有字面量 `initial` 条目时,只在内存中展开,不写文件。发现和投影诊断保留在 `x-cordis` 中,包括未知 Config 和部分约束。[CLI schema dump 参考](../../../apps/cli/reference/README.zh.md#config-schema-dump)负责说明输出字段和编辑语义。
80
+
81
+ 投影器使用 Ajv 根据生成的 schema 检查字面量默认值,以保留原生省略行为,不执行原生验证器或 transform 回调。正则兼容性检查、不支持的情况及递归默认值分析会产生显式限制说明。不透明的输入转换和 lazy 元数据副作用会放宽验证,而不是重放原生修改。非 JSON 默认值或展示注释会被省略并附上限制说明,不丢弃结构 schema;无法表示的默认值使省略接受性保持未知,必填字段除外。这些依赖仅在收集运行时加载。导入、Config getter 和 lazy builder 仍会执行可信代码;收集不是沙箱。profile 解析拦截不得重叠。收集器返回前释放自己的拦截,而 Node 仍缓存导入的模块;运行时创建的插件和 Agent preset 实例不在发现范围内。
82
+
83
+ ### 读取插件展示元信息
84
+
85
+ `resolvePluginResource(specifier, parentURL)` 通过活动的 Node ESM 解析器将插件模块或导出资源解析为本地文件路径,不执行其代码。解析器不可用或资源无法解析为本地文件时会抛出错误。
86
+
87
+ 使用 `readPluginMeta(specifier, parentURL)` 或 `ctx.pluginPackages.metaOf(specifier, parentURL)` 读取已安装包的展示文本,无需导入或激活插件。查询使用完整包标识与调用方的解析基准,并遵循 Node exports。文件路径与文件 URL 不解析资源,直接返回无元信息。包根标识缺失的 locale 字段回退到可访问的 `package.json`;子路径标识从不读取 `package.json`。格式错误的元信息返回 `error` 诊断。结果保留翻译,由 Client 选择语言。即使 locale 文本完整,读取器也会加载图片 data URL:包根使用清单 `icon`,省略该字段时使用 `<包名>/icon`;子路径使用 `<标识>/icon`。图标出错时,保留有效文本并附上诊断。作者格式见[插件展示元信息](../../../docs/cookbook/adding-a-package.zh.md#plugin-display-metadata)。
88
+
89
+ `ON_DEMAND_BUNDLES` 列出仅按需安装的官方公开包。`OFFICIAL_ON_DEMAND_CATALOG` 内嵌这些包拥有的本地化元信息和图标,支持离线发现;`pnpm gen-official-bundle-catalog` 显式更新生成内容,`verify-official-bundle-catalog` 检查资源完整性与新鲜度;构建只读取已提交的目录,不导入提供者代码。目录不保存安装目标;[插件管理器](../plugin-manager/README.zh.md#use-this-package)根据当前版本和安装位置确定目标,并使用普通组合包安装器。[产品用途检查](../../../scripts/verify-product-use.ts)将随附和按需选择与 Web 组合,但不执行提供者。
90
+
91
+ <a id="startup-and-reload-failures"></a>
92
+ ### 启动与重载失败
93
+
94
+ profile 重载返回未变化的已有故障诊断,不让无关修改因此失败。新增未激活条目、配置或 fiber 变化、诊断变化都会使重载失败;被移除的 fiber 仍须完成释放。显式启用的目标必须成功激活,即使它的故障早于本次操作。成功重载在生命周期结束及诊断检查通过后发出 `app-boot/config-reload`,包括未启用 HMR 时的程序化更新。事件不携带 diff 或解析后的配置。 成功重载在生命周期结束及诊断检查通过后返回;仅 volatile 的条目变化由 Loader 在更新过程中提交。
95
+
96
+ Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如果已启用的 required 条目无法激活,`boot()` 会在释放资源后以 `StartupError` 拒绝。独立管理生命周期的 logger exporter 会保留异步资源释放期间的警告和错误记录,并在 `boot()` 结算前释放。其消息分组列出所有失败插件和等待的服务,标记 required 条目,并保留原始堆栈、嵌套原因和聚合错误成员。CLI 仅输出该消息一次,并在保存[完整启动诊断](../../../apps/cli/reference/README.zh.md#startup-diagnostics)后以退出码 1 结束;其他异常保留正常堆栈输出。表中的“终止启动”指释放已挂载插件并以非零码退出,不报告就绪;“继续”指保留成功运行的插件。后续配置 HMR 不会再次执行 required 启动审计,也不会回滚整个更新。
97
+
98
+ | 失败模式 | Optional 条目启动时 | Required 条目启动时 | 后续配置 HMR |
99
+ |---|---|---|---|
100
+ | 根配置或必需 overlay 缺失、不可读、格式错误,或包含无效条目 | 终止启动 | 终止启动 | 拒绝格式错误或无效的实时 patch,不改变运行中的配置;有效修改可以应用 |
101
+ | 模块 import 失败或模块求值抛出异常 | 警告;继续 | 终止启动 | 报告错误;保留成功的兄弟插件;修正 import 后可以激活 |
102
+ | 插件配置 schema 校验失败 | 警告;继续 | 终止启动 | 新条目保持未激活;现有条目保留原实例与配置;有效修正可以应用 |
103
+ | 配置 `!!js` 求值抛出异常 | 警告;继续 | 终止启动 | 报告错误;保留成功的兄弟插件;有效修正后可以激活 |
104
+ | `disabled: !!js` 求值抛出异常 | 警告;继续 | 终止启动 | 报告求值错误,不将条目当作已禁用;有效修正后可以激活 |
105
+ | 同步 `apply()` throw | 警告;继续 | 终止启动 | 报告错误;保留成功的兄弟插件;修正配置后可以激活 |
106
+ | 异步 `apply()` throw | 结算后警告;继续 | 结算后终止启动 | 结算后报告错误;保留成功的兄弟插件;修正配置后可以激活 |
107
+ | 注入的服务不可用 | 警告;继续,条目等待依赖 | 终止启动 | 条目继续等待;补上缺失的提供方后可以激活 |
108
+ | HTTP 端口绑定失败 | 警告;继续,但该端点不可用 | 终止启动 | 进程继续运行,但失败的端点不可用;修正配置后可以恢复 |
109
+ | 脱离 `apply()` 返回 Promise 的异步任务产生未处理 rejection | 致命错误:释放应用并以非零码退出 | 致命错误:释放应用并以非零码退出 | 致命错误:释放应用并以非零码退出,与条目 id 无关 |
110
+ | 同步回调(流的 `'data'` 监听器、定时器)在进程生命周期任一时刻抛出未捕获异常 | 致命错误:释放应用并以非零码退出;失败的操作不会被恢复 | 致命错误:释放应用并以非零码退出;失败的操作不会被恢复 | 致命错误:释放应用并以非零码退出,与条目 id 无关 |
111
+ | 条目缺失或被显式禁用 | 忽略 | 忽略 | 不激活该条目;不执行 required 启动审计 |
112
+
113
+ 上面的 required 列表包含 `modules` 与 `connection`;只要其中一个已启用条目失败,Web 就无法成功启动。Optional 提供方失败也可能使 required 消费方无法激活。现有条目的新配置在更新前被 schema 校验拒绝,并不等于对兄弟插件的变更做事务回滚。
114
+
115
+ [Web 进程矩阵](../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts)和[启动验收测试](../../../apps/cli/tests/profiles/web/tests/web-best-effort-startup.expected.e2e.ts)通过随附 Web profile 验证这些结果;[app-boot 测试](tests/app-boot.spec.ts)还覆盖根 Include 失败。
116
+
117
+ 如果你的应用持有终端,它可以在进程退出前把终端交还,你的 shell 绝不会残留在 raw 模式。交还过程有界:卡住的清理只会延迟致命退出,而不会取消它。
118
+
119
+ ### 告诉 agent(智能体)harness 所在位置
120
+
121
+ 当你的应用启动模型驱动的 agent 时,你可以告诉 agent DSH 实现代码 checkout 的位置:它得知该路径,也知道不得据此推断工作目录——它应使用 `pwd`。这条指示在系统提示词靠前位置出现一次。没有系统提示词服务的应用会跳过;开发环境中,重新加载系统提示词后它会消失,直至下次启动。
122
+
123
+ -----
124
+
125
+ <a id="understand-the-implementation"></a>
126
+ ## 理解实现
127
+
128
+ <details>
129
+ <summary>实现细节——点击展开</summary>
130
+
131
+ 本节解释上述结果如何实现,并指出实现它们的代码位置;这里的内容面向开发者,使用本包并不需要。
132
+
133
+ ### 设计说明
134
+
135
+ - **运行时版本。** `getDshRuntimeVersion()` 通过文件系统路径读取本包清单,也支持可执行文件内的虚拟文件系统;版本缺失或无效时会失败,而不会绕过兼容性检查。
136
+ - **Profile 启动数据。** `ctx.profileContext` 只包含 profile 位置、启动时组合包名称、已解析的调用级 overlay 与遥测退出值。`readProfilePatches()` 组合传入的启动 profile,或读取这些位置上的当前文件;调用方负责调度和应用结果。
137
+ - **进程内模块解析。** launcher 在挂载 profile 条目前,将 runtime resolution 安装到 Node 的 ESM 与 CommonJS 内部 resolver。exports、conditions、子路径、模块缓存和错误码仍由 Node 负责;路由后的 ESM 失败报告原始 importer。显式 CommonJS `paths` 始终保留原生查询,包括指向 profile 内的路径。
138
+ - **链接目录。** profile 链接到树外目录时,其下的 importer 参与逐层 peer 查询,即使目标没有自身的 `package.json`。在每个 `D/node_modules` 位置,当前 `D/package.json` 的 peer 包名若存在于运行时表,就使用运行时包;其他包名查询物理候选。更近的物理包先于后续 peer 声明,peer 位置无需物理 `node_modules`。installation 作用域包目录不参与 linked 拦截,重叠 root 不改变 importer 的查询顺序([规则](../../../.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.zh.md))。
139
+ - **包元数据。** `ctx.pluginPackages.packageOf` 定位所属包,不加载代码,也不要求导出 `package.json`;子路径选择其所属包,不校验该文件。安装 runtime resolution 后,即使查询未命中也以其选包规则为准。仅安装服务而不提供 runtime resolution 的底层嵌入方保留原生查询。展示元数据使用上文另述的入口感知读取器。
140
+ - **两个 Loader builtin。** `mountRootInclude` 把 `cordis:include` 与 `cordis:group` 注册为 Loader builtin:group 行能把一个提供方与它的消费方放进同一个 `isolate` realm,而位于本工作区之外的 agent preset 无法按名称解析 `@deepseek-ai/cordis-plugin-group`。两者都通过宿主的模块管线加载,而非被包含树自身的说明符解析。
141
+ - **由 consumer 持有严格语义。** 普通 Loader group 保留成功 sibling。App-boot 在首次结算后应用全局 required-entry policy;agent preset 与动态多 entry 组合在需要 all-or-nothing setup 时,持有并拆卸各自的独立 Loader 子树。App-boot 读取 failed fiber 来报告已记录的错误,并在一个进程检查点内合并 Loader 重复的 rejection 通知。
142
+ - **唯一 runtime resolution。** 安装优先、有序 bundle 逐根 breadth-first 遍历生成运行时表。runtime 解析不创建链接;runtime resolution 条目占据 `$DSH_HOME/profiles/node_modules` 上各自的包名位置,其余包名把该目录当作普通祖先。profile 加载时删除 Link 后端发布版写进 profile 的 `.dsh-module-fallback` 投影;pnpm 安装的包保留。package `imports` 选中的外部 bare target 使用相同的选包顺序,映射、conditions 和精确 target 解析仍由 Node 负责。完整后继 runtime resolution 可以原子地增加 package name、移除 profile 范围的映射和 profile 本地包名、更新 linked root 集合;保留映射的规范化目录、版本和作用域不变。安装范围的锚点不变;profile 映射可以更换声明来源。[插件管理器](../plugin-manager/README.zh.md)在包操作之后发布后继代([决策](../../../.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md))。
143
+ - **移除链接拦截。** 后继 generation 可以移除 linked root,无需重启。目录不再被任何剩余 root 覆盖时,后续请求使用原生查询,可能找到开发副本,也可能报告缺包。已有模块引用和 Node 缓存保持不变。同名、同目标可以重新加入;曾发布的名称改指向不同目标时,即使中间移除过也会被拒绝([generation 规则](../../../.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md#immutable-generations))。
144
+ - **应用自有 profile。** 应用自有 profile 使用相同的 runtime resolution。目标位于当前 profile 目录内的链接(包括 pnpm store 链接)不算外部 root,即使 profile 位于共享 profiles 树外。解析过程不修改其 `node_modules`;已安装包由 pnpm 管理。
145
+ - **自有 Worker。** Worker 构建 banner 会在业务 bundle 前导入 `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap`。每个 Worker 在自己的 isolate 中安装结构化克隆的 runtime resolution。bootstrap bundle 不静态导入任何包。源码 Worker 入口保留自包含依赖,第三方 Worker 不接受注入。
146
+ - **更新完成。** App boot 通过 `internal/update` waterfall 观察重启失败。实时 patch 重载在检查激活状态前等待配置树中的 fiber;单独调用 `Fiber.update()` 或 `Entry.update()` 不能确定重启成功。
147
+ - **单一 rejection 检查点。** `inactiveEntries` 把折入启动诊断的确切原因保持到下一个进程级 rejection 检查点可见,使 `installFailLoud` 能合并 Loader 的重复通知,而所有无关的未处理 rejection 仍然致命。
148
+ - **两阶段失败标签。** 除启动审计失败外,`boot()` 区分 `host preparation failed`(`prepare` 在任何配置树条目挂载前抛出)与 `plugin tree failed to load`,并追加最深层插件错误的堆栈。插件诊断保留嵌套原因和聚合错误中的各项失败;原因链出现循环时会停止遍历,但不会替换原始错误。
149
+
150
+ 启动错误还保留未激活条目的元数据和原始启动警告、错误记录,不保留 Loader tree。其 `entries` 和 `startup` 字段不可枚举:直接访问和完整诊断报告保留这些字段,常规错误检查输出则省略它们。只有等待条目时没有 `cause`;存在已记录错误时,`AggregateError` 保留其原始值。收集器在 Loader 挂载前通过 logger 收集导入错误,因为这些导入尚无 failed Fiber。启动结算后会移除临时 exporter。
151
+
152
+ ### Helper 行为
153
+
154
+ 每个导出各负责启动的一个阶段:配置解析与快照回放、分层环境加载、明确报错的保护机制、激活审计、patch 解析、根 include 挂载、配置 dump 渲染、profile 组合,以及 harness 源码段落。各导出的约定在代码中,不在本 README——见 [`src/index.ts`](src/index.ts) 与 [`src/profile.ts`](src/profile.ts)。
155
+
156
+ ### 源码地图
157
+
158
+ | 文件 | 职责 |
159
+ |---|---|
160
+ | [`src/index.ts`](src/index.ts) | 启动 helper:配置解析、环境加载、会明确报错的保护机制、激活审计、patch 解析、配置 dump、harness 源码段落 |
161
+ | [`src/profile.ts`](src/profile.ts) | profile 发现、初始化、组合包解析、runtime resolution 构造 |
162
+ | [`src/profile-plugins.ts`](src/profile-plugins.ts) | 已安装依赖、bundle 启用策略与 manifest 更新 |
163
+ | [`src/profile-sanitize.ts`](src/profile-sanitize.ts) | profile patch 备份与恢复 bundle 启用状态 |
164
+ | [`src/config-schema/`](src/config-schema/) | Profile schema 生成、发现、原生投影与结果类型 |
165
+ | [`src/profile-resolution/`](src/profile-resolution/) | 运行时 resolver、package metadata 服务与构建后 Worker bootstrap |
166
+
167
+ </details>
168
+
169
+ -----
170
+
171
+ <a id="further-exploration"></a>
172
+ ## 进一步探索
173
+
174
+ 当包级约定不够用时阅读以下页面。它们从共享启动机制逐步进入组合模型及其背后的决策证据。
175
+
176
+ - [Cordis 入门](../../../docs/cordis-primer.zh.md)——Loader、`!!js` 配置表达式,以及 include/group 语义。
177
+ - [dsh 应用](../../../apps/cli/README.zh.md)——消费这些 helper 的 `dsh` bin。
178
+ - [dsh-cmdline](../cmdline/README.zh.md)——各 bin 使用的启动器到应用命令行交接。
179
+ - [Profile 组合包](../../bundle/README.zh.md)——组合进 `dsh --profile` 的可安装 patch 层。
180
+ - [dsh-home-paths](../../util/home-paths/README.zh.md)——harness home 解析器(`resolveDshHome`)。
181
+ - [配置来源归属](../../../.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md)——被发现的文件为何不得决定 bootstrap 行为。
182
+ - [Profile 插件组合包](../../../.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md)——profile 与组合包组合设计。
183
+ - [用户 patch HMR 测试](tests/user-patches.spec.ts)——实时 patch 行为与原生文件系统投递的验证归属。
184
+
185
+ -----
186
+
187
+ <a id="model-experience"></a>
188
+ ## 模型体验
189
+
190
+ 模型通过此包加载的插件树间接受影响——只有该树贡献模型上下文;唯一贡献模型可见文本的导出 `addHarnessSourceSection`,也只有在消费方启动后调用它时才会产生影响。
191
+
192
+ #### KV Cache 影响
193
+
194
+ 启动本身不改变请求前缀。`addHarnessSourceSection` 将源码路径放在第一方可复用指令之后,因此工具与配置一致时,不同 checkout 不会改变前置字节。不保证提供方复用缓存。
195
+
196
+ ## 已知限制与延期工作
197
+
198
+ <a id="known-limitations-and-deferred-work"></a>
199
+
200
+ 这些限制说明此启动库在何时不合适,或何时需要特别注意。它们是当前包约束,不是任务积压。
201
+
202
+ - **运行时解析依赖 Node 内部机制**——受支持的 Node 版本需要 native builtin access addon 和可执行兼容验证。只有构建后的 Harness 自有 Worker 接收 runtime resolution bootstrap;第三方 Worker 与自定义 `vm` linker 保持原生解析。
203
+ - **重新链接 profile 包需要重启**——Node 缓存真实路径,因此改变 profile 链接或依赖链接的目标需要重启进程。
204
+ - **链接作用域以记录的真实目录为准**——提升后的依赖若在所有 linked root 之外,就使用原生 Node。每次读取 peer 都不会使 Node 缓存失效,也不会改变被监视的文件。
205
+ - **源码启动只安装 ESM 钩子**——CommonJS 请求仍需要 package exports 选中的 JavaScript 文件,解析器不会补出缺失的构建产物。
206
+ - **快照回放替换仅识别特定 basename**——只有以 `cordis.yml` 或 `cordis.yaml` 结尾的配置会映射到同级 `cordis.snapshot.yml`;自定义配置名称需要调用方自行选择。
207
+ - **环境发现以启动为界**——`loadLayeredEnv` 只读取一次调用目录与 harness home 中的 `.env`;它不搜索父目录,也不跟随之后选择的 workspace。`loadEnv` 仍是非产品 bin 使用的单目录 helper。
208
+ - **用户 patch 会替换匹配到的整个配置**——按 id 定位的 patch 不做深度合并,因此 profile 覆盖必须重述需要保留的组合包字段。
209
+
210
+ <a id="dev-note"></a>
211
+ ### 开发备注
212
+
213
+ <details>
214
+ <summary>维护者的工作上下文——点击展开</summary>
215
+
216
+ 本开发备注是维护者的工作上下文:开放设计问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文、包代码和相关 Agent Note 为准。
217
+
218
+ #### 待定:YAML 配置 dump 稳定性
219
+
220
+ `renderConfigDump` 的输出是一份可加载的 YAML 文档,其 `# ==` 来源注释与 `!!js` 原样渲染服务于 `--dump-config` 诊断。任何内容都不承诺跨包版本的字节稳定性;在程序化消费该输出之前,请决定 dump 是否成为序列化约定。JSON Schema 输出遵循单独的 [pre-stable 兼容性规则](../../../apps/cli/reference/README.zh.md#config-schema-dump)。
221
+
222
+ </details>