@khorsheed/dsh-local-agent-dsh 0.2.0-rc.1 → 0.2.0

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/lib/provision.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
- import { dirname, isAbsolute, join } from "node:path";
3
- import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
- import { homedir } from "node:os";
2
+ import { dirname, isAbsolute, join, relative } from "node:path";
3
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
4
  import yaml from "js-yaml";
6
5
  //#region lib/types/provision.js
7
6
  /**
@@ -11,29 +10,38 @@ import yaml from "js-yaml";
11
10
  * `<scoped home>/profiles/<name>` — a manifest listing the official
12
11
  * `@deepseek-ai/dsh-base` layer, the family headless composition as the
13
12
  * profile's own patch layer (copied verbatim from the headless bundle's
14
- * `cordis.patch.yml`), and a single symlink resolving
15
- * `@khorsheed/dsh-local-agent-dsh-headless` from the sub-profile's
16
- * node_modules (the loader resolves the patch's insert rows through it).
13
+ * `cordis.patch.yml`), and `@khorsheed/dsh-local-agent-dsh-headless` in
14
+ * the sub-profile's node_modules (the loader resolves the patch's insert
15
+ * rows through it) — a copy of the bundle for the host profile, a symlink
16
+ * for the `-unit` twin and for the pinned single-profile form (see
17
+ * `HeadlessBundleMount`).
17
18
  * Everything else the boot needs (dsh-base and its whole dependency graph,
18
19
  * cordis, the healed `profiles/node_modules` fallback) is resolved from the
19
20
  * dsh installation anchor automatically, so provisioning costs no pnpm
20
21
  * install.
21
22
  *
22
- * A sub-profile may additionally carry a PRESET ROSTER: one appended patch
23
- * layer inserting `@deepseek-ai/dsh-agent-presets` with a `default` preset
24
- * id. Without it the sub-dsh's model-facing rows come from the host plane and
25
- * the agent reads them off the global layer — which is why "the same harness
26
- * under two presets" was not a thing a condition could ask for. With it, the
27
- * scope directory IS the capability face: two scopes whose rosters name two
28
- * presets are two subjects, and the difference is a file a person can read.
23
+ * A sub-profile may additionally compose an AGENT PRESET (host 0.2.0's
24
+ * declarative form): the bundle package that declares the preset — an
25
+ * `@deepseek-ai/dsh-agent-preset` row in its own bundle patch — joins the
26
+ * manifest's bundle list, and one appended patch layer inserts
27
+ * `@deepseek-ai/dsh-agent-preset-registry` with that preset as its
28
+ * `default`. Without it the sub-dsh's model-facing rows come from the host
29
+ * plane and the agent reads them off the global layer — which is why "the
30
+ * same harness under two presets" was not a thing a condition could ask for.
31
+ * With it, the scope directory IS the capability face: two scopes naming two
32
+ * presets are two subjects, and the difference is files a person can read.
29
33
  *
30
- * Where the preset DIRECTORY lives is the scope's business too. The roster
31
- * derives a user root of `<$DSH_HOME>/.agent-presets` and the sub-dsh runs
32
- * with `DSH_HOME` pointed at the scoped home, so a scope that keeps its own
33
- * copy there is resolved identically on the host and inside an evaluation
34
- * unit (which bind-mounts the scope, and nothing else). `provisionDshScope`
35
- * below is the entry that puts the copy there, records what the scope
36
- * composes in a file of its own, and rosters it with no `roots` at all.
34
+ * Where the preset PACKAGE lives is the scope's business too. The scope keeps
35
+ * its own copy at `<scoped home>/preset-packages/<id>` and the sub-profile
36
+ * resolves it through a RELATIVE symlink in its own `node_modules`, so the
37
+ * package is found identically on the host and inside an evaluation unit
38
+ * (which bind-mounts the scope, and nothing else). The copy comes from the
39
+ * evaluation instance's own installation of the same package, which is also
40
+ * what the instance's capability catalog measures. `provisionDshScope` below
41
+ * is the entry that makes the copy, records what the scope composes in a
42
+ * file of its own, and composes it. (0.1.x wrote a roster row for
43
+ * `@deepseek-ai/dsh-agent-presets` and a `<scoped home>/.agent-presets/<id>`
44
+ * directory instead; 0.2.0 ships neither the roster nor the directory scan.)
37
45
  *
38
46
  * It may also carry a PERMISSION BOUNDARY: one appended patch layer pinning
39
47
  * `sandbox-policy`'s mode and `user-approval`'s policy to one of dsh's own
@@ -42,7 +50,7 @@ import yaml from "js-yaml";
42
50
  * sub-dsh on a developer's machine and the wrong one inside an evaluation
43
51
  * unit, where the container IS the boundary and nobody is there to answer an
44
52
  * approval prompt. The layer is a FILE in the scope directory for the same
45
- * reason the roster is: a scoped home bind-mounted into a unit carries its
53
+ * reason the preset layer is: a scoped home bind-mounted into a unit carries its
46
54
  * own boundary with it, and the value never has to ride the spawn env (where
47
55
  * every name enters the unit's composite fingerprint) or be baked into an
48
56
  * image.
@@ -74,40 +82,44 @@ const APPROVAL_FOR = {
74
82
  "workspace-write": "ask",
75
83
  "danger-full-access": "never"
76
84
  };
77
- /** The roster row's id inside the sub-profile patch (and the marker this module rewrites). */
78
- const PRESET_ROSTER_ROW_ID = "agent-presets";
85
+ /** The registry row's id inside the sub-profile patch. */
86
+ const PRESET_REGISTRY_ROW_ID = "agent-preset-registry";
79
87
  /**
80
- * The roster package the roster row names.
88
+ * The registry package the registry row names.
81
89
  *
82
90
  * Deliberately NOT symlinked into the sub-profile the way the headless bundle
83
91
  * is. The headless bundle is a `@khorsheed` package the dsh installation does
84
- * not carry, so it must be linked in from the plugin installation; the roster
85
- * is an OFFICIAL package, already in the installation anchor's closure beside
86
- * `@deepseek-ai/dsh-base`. Linking a second copy would give it a second
87
- * `@deepseek-ai/cordis` — and cordis does service lookup and type checks by
88
- * instance identity, so the symptom would be silently missing services rather
89
- * than an error (the same failure the dual-filesystem contract in the README
90
- * describes for the bundle). A deployment whose anchor genuinely lacks the
91
- * roster gets the loader's own "cannot resolve" message, which names the
92
+ * not carry, so it must be linked in from the plugin installation; the
93
+ * registry is an OFFICIAL package, already in the installation anchor's
94
+ * closure beside `@deepseek-ai/dsh-base`. Linking a second copy would give it
95
+ * a second `@deepseek-ai/cordis` — and cordis does service lookup and type
96
+ * checks by instance identity, so the symptom would be silently missing
97
+ * services rather than an error. A deployment whose anchor genuinely lacks the
98
+ * registry gets the loader's own "cannot resolve" message, which names the
92
99
  * module better than provisioning could.
93
100
  */
94
- const PRESET_ROSTER_MODULE = "@deepseek-ai/dsh-agent-presets";
95
- /** A preset id doubles as a directory name — the roster's own rule. */
101
+ const PRESET_REGISTRY_MODULE = "@deepseek-ai/dsh-agent-preset-registry";
102
+ /** The declarative preset row a preset bundle package carries in its patch. */
103
+ const PRESET_ROW_MODULE = "@deepseek-ai/dsh-agent-preset";
104
+ /** A preset id doubles as a directory name inside the scope. */
96
105
  const SUB_PROFILE_PRESET_ID_RE = /^[a-z0-9][a-z0-9-]*$/;
97
- /** Header line that opens the generated roster layer (the parse anchor). */
98
- const PRESET_BLOCK_HEADER = "# --- preset roster (written by local-agent-dsh provisioning) ---";
106
+ /** An npm package name the sub-profile manifest may list as a bundle. */
107
+ const PACKAGE_NAME_RE = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
108
+ /** Header line that opens the generated preset layer (the parse anchor). */
109
+ const PRESET_BLOCK_HEADER = "# --- agent preset (written by local-agent-dsh provisioning) ---";
99
110
  /** Header line that opens the generated permission layer (the parse anchor). */
100
111
  const PERMISSION_BLOCK_HEADER = "# --- permission boundary (written by local-agent-dsh provisioning) ---";
101
112
  /** The sub-profile's bundle layer list: the official base layer. The headless
102
113
  * composition rides the profile's own patch layer (see the module doc). */
103
114
  const SUB_PROFILE_BUNDLES = ["@deepseek-ai/dsh-base"];
104
115
  /** The sub-profile manifest the boot reads. */
105
- function subProfileManifest(profileName) {
116
+ function subProfileManifest(profileName, presetPackage) {
117
+ const bundles = presetPackage === void 0 ? [...SUB_PROFILE_BUNDLES] : [...SUB_PROFILE_BUNDLES, presetPackage];
106
118
  return JSON.stringify({
107
119
  name: `dsh-profile-${profileName}`,
108
120
  private: true,
109
121
  dependencies: {},
110
- dsh: { profile: { bundles: [...SUB_PROFILE_BUNDLES] } }
122
+ dsh: { profile: { bundles } }
111
123
  }, void 0, 2) + "\n";
112
124
  }
113
125
  /**
@@ -122,55 +134,51 @@ function resolveHeadlessBundleDir(config = {}) {
122
134
  const manifestPath = createRequire(import.meta.url).resolve("@khorsheed/dsh-local-agent-dsh-headless/package.json");
123
135
  return dirname(manifestPath);
124
136
  }
125
- /** YAML-quote a scalar the roster config carries (paths and ids are operator input). */
137
+ /** YAML-quote a scalar the generated layers carry (ids are operator input). */
126
138
  function yamlString(value) {
127
139
  return JSON.stringify(value);
128
140
  }
129
141
  /**
130
- * The roster layer appended to the sub-profile's patch, or `''` when the
142
+ * The preset layer appended to the sub-profile's patch, or `''` when the
131
143
  * sub-profile composes no preset.
132
144
  *
145
+ * One insert of the 0.2.0 preset registry with the preset as its `default`:
146
+ * every sub-dsh session that names no preset joins this one (the headless
147
+ * agent loader mounts it through the registry's `mount`). The preset's own
148
+ * declaration rides the bundle layer — the package that declares it is in
149
+ * the manifest's bundle list — so this layer never repeats the composition.
150
+ *
133
151
  * It is a SEPARATE patch operation appended after the headless bundle's own
134
152
  * list, which is what keeps the two independent: an upgraded headless bundle
135
- * rewrites its half and the roster survives, and dropping the preset drops
153
+ * rewrites its half and the preset survives, and dropping the preset drops
136
154
  * exactly these lines.
137
- * @param preset - the roster to compose, or undefined for none.
155
+ * @param preset - the preset to compose, or undefined for none.
138
156
  * @returns the YAML text to append (already newline-terminated), or `''`.
139
157
  */
140
- function presetRosterLayer(preset) {
158
+ function presetRegistryLayer(preset) {
141
159
  if (preset === void 0) return "";
142
160
  if (!SUB_PROFILE_PRESET_ID_RE.test(preset.id)) throw new Error(`provision-dsh: preset id ${JSON.stringify(preset.id)} must match ${String(SUB_PROFILE_PRESET_ID_RE)} (it is a directory name)`);
143
- const lines = [
161
+ return `${[
144
162
  "",
145
163
  PRESET_BLOCK_HEADER,
146
164
  "# The sub-dsh composes every session from this preset; the headless agent",
147
- "# loader joins the roster in its agent setup. Remove this layer and the",
148
- "# agent falls back to the global layer.",
165
+ "# loader mounts it through the registry. Remove this layer and the agent",
166
+ "# falls back to the global layer.",
149
167
  "- insert:",
150
- ` - id: ${PRESET_ROSTER_ROW_ID}`,
151
- ` name: '${PRESET_ROSTER_MODULE}'`,
168
+ ` - id: ${PRESET_REGISTRY_ROW_ID}`,
169
+ ` name: '${PRESET_REGISTRY_MODULE}'`,
152
170
  " config:",
153
171
  ` default: ${yamlString(preset.id)}`
154
- ];
155
- if (preset.includeShippedRoot !== void 0) lines.push(` includeShippedRoot: ${String(preset.includeShippedRoot)}`);
156
- if (preset.includeUserRoot !== void 0) lines.push(` includeUserRoot: ${String(preset.includeUserRoot)}`);
157
- if (preset.roots !== void 0 && preset.roots.length > 0) {
158
- lines.push(" roots:");
159
- for (const root of preset.roots) {
160
- lines.push(` - path: ${yamlString(root.path)}`);
161
- if (root.trust !== void 0) lines.push(` trust: ${root.trust}`);
162
- }
163
- }
164
- return `${lines.join("\n")}\n`;
172
+ ].join("\n")}\n`;
165
173
  }
166
174
  /**
167
175
  * The preset id a provisioned sub-profile's patch composes, read back from
168
- * the file. The roster layer is generated, so it is also parseable: a lock
176
+ * the file. The preset layer is generated, so it is also parseable: a lock
169
177
  * check or a status surface can answer "which preset does this scope run?"
170
178
  * without booting the sub-dsh.
171
179
  * @param homeDir - the harness scoped home.
172
180
  * @param profileName - the sub-profile name (default {@link DEFAULT_SUB_PROFILE_NAME}).
173
- * @returns the preset id, or undefined when the sub-profile composes no roster.
181
+ * @returns the preset id, or undefined when the sub-profile composes no preset.
174
182
  */
175
183
  function readSubProfilePreset(homeDir, profileName = DEFAULT_SUB_PROFILE_NAME) {
176
184
  const patchPath = join(homeDir, "profiles", profileName, PROFILE_PATCH_FILENAME);
@@ -267,57 +275,114 @@ function ensureSymlink(link, target) {
267
275
  rmSync(link, { force: true });
268
276
  symlinkSync(target, link);
269
277
  }
278
+ /** Suffix of the evaluation-unit twin of a sub-profile (see `unitHeadlessBundleDir`). */
279
+ const UNIT_PROFILE_SUFFIX = "-unit";
280
+ /**
281
+ * The profile name an evaluation UNIT launches for this config: the
282
+ * `-unit` twin when one is configured, else the one sub-profile.
283
+ * @param config - provisioning config.
284
+ * @returns the profile name a container round passes to `--profile`.
285
+ */
286
+ function unitSubProfileName(config = {}) {
287
+ const profileName = config.profileName ?? "headless-local-agent-dsh";
288
+ return config.unitHeadlessBundleDir === void 0 ? profileName : `${profileName}${UNIT_PROFILE_SUFFIX}`;
289
+ }
290
+ /** Write one file only when its content differs. */
291
+ function writeIfChanged(path, content) {
292
+ if (!existsSync(path) || readFileSync(path, "utf8") !== content) writeFileSync(path, content);
293
+ }
294
+ /**
295
+ * Make `target` a byte-for-byte copy of the headless bundle `source`. A
296
+ * symlink left by an earlier scheme is replaced; an identical copy is left
297
+ * untouched, so re-provisioning before every round costs one comparison.
298
+ */
299
+ function ensureBundleCopy(target, source) {
300
+ let stat;
301
+ try {
302
+ stat = lstatSync(target);
303
+ } catch (error) {
304
+ if (error.code !== "ENOENT") throw error;
305
+ }
306
+ if (stat?.isSymbolicLink() === true) rmSync(target, { force: true });
307
+ else if (stat !== void 0 && presetTreesEqual(source, target)) return;
308
+ if (presetTreeFiles(source) === void 0) throw new Error(`provision-dsh: headless bundle ${source} is missing or not a plain tree of files`);
309
+ mirrorPresetTree(source, target);
310
+ }
311
+ /** Provision one sub-profile directory against one headless bundle directory. */
312
+ function provisionOneSubProfile(homeDir, profileName, bundleDir, patchSourceDir, config, mount) {
313
+ const profileDir = join(homeDir, "profiles", profileName);
314
+ mkdirSync(profileDir, { recursive: true });
315
+ const presetPackage = config.preset?.package;
316
+ if (presetPackage !== void 0 && !PACKAGE_NAME_RE.test(presetPackage)) throw new Error(`provision-dsh: preset package name ${JSON.stringify(presetPackage)} is not an npm package name`);
317
+ writeIfChanged(join(profileDir, "package.json"), subProfileManifest(profileName, presetPackage));
318
+ writeIfChanged(join(profileDir, PROFILE_PATCH_FILENAME), readFileSync(join(patchSourceDir, HEADLESS_PATCH_FILENAME), "utf8") + presetRegistryLayer(config.preset) + permissionBoundaryLayer(config.permissions) + (config.inheritModelConfiguration ? "\n# Selected local model; values and credentials stay in the launch environment.\n- id: llm-pi-ai\n config:\n providers: !!js JSON.parse(process.env.DSH_LOCAL_AGENT_MODEL_CONFIG || \"{}\").piAi?.providers ?? {}\n- id: llm-deepseek\n config: !!js JSON.parse(process.env.DSH_LOCAL_AGENT_MODEL_CONFIG || \"{}\").deepseek ?? {}\n" : ""));
319
+ const bundleLink = join(profileDir, "node_modules", "@khorsheed", "dsh-local-agent-dsh-headless");
320
+ mkdirSync(dirname(bundleLink), { recursive: true });
321
+ if (mount === "copy") ensureBundleCopy(bundleLink, bundleDir);
322
+ else ensureSymlink(bundleLink, bundleDir);
323
+ if (presetPackage !== void 0 && config.preset !== void 0) {
324
+ const presetLink = join(profileDir, "node_modules", ...presetPackage.split("/"));
325
+ mkdirSync(dirname(presetLink), { recursive: true });
326
+ ensureSymlink(presetLink, relative(dirname(presetLink), scopePresetPackageDir(homeDir, config.preset.id)));
327
+ }
328
+ return profileDir;
329
+ }
270
330
  /**
271
331
  * Provision (idempotently) the sub-dsh headless profile in the harness
272
332
  * scoped home: manifest, the headless patch as the profile's own patch
273
- * layer, and the bundle symlink. A file whose content already matches is
274
- * left untouched, so re-running after a restart is a no-op; a drifted file
275
- * (an upgraded headless bundle, or a profile provisioned by the pre-T6
276
- * scheme whose manifest listed the headless bundle as a layer) is rewritten,
277
- * which heals it before the sub-dsh boots.
333
+ * layer, and the headless bundle (a copy; a symlink in the `-unit` twin or under a lone `headlessBundleDir` pin) — plus, when a preset package is composed,
334
+ * the relative link resolving it from the scope's own copy, and, when
335
+ * `unitHeadlessBundleDir` is configured, the `-unit` twin a container round
336
+ * launches. A file whose content already matches is left untouched, so
337
+ * re-running after a restart is a no-op; a drifted file (an upgraded headless
338
+ * bundle, or a profile provisioned by an earlier scheme) is rewritten, which
339
+ * heals it before the sub-dsh boots.
278
340
  * @param homeDir - the `dsh` harness's scoped home (its `$DSH_HOME` root).
279
341
  * @param config - profile naming and bundle-dir overrides.
280
- * @returns the absolute sub-profile directory.
342
+ * @returns the absolute (host) sub-profile directory.
281
343
  */
282
344
  function provisionDshSubProfile(homeDir, config = {}) {
283
345
  const profileName = config.profileName ?? "headless-local-agent-dsh";
284
- const bundleDir = resolveHeadlessBundleDir(config);
285
- const profileDir = join(homeDir, "profiles", profileName);
286
- mkdirSync(profileDir, { recursive: true });
287
- const manifestPath = join(profileDir, "package.json");
288
- const manifestContent = subProfileManifest(profileName);
289
- if (!existsSync(manifestPath) || readFileSync(manifestPath, "utf8") !== manifestContent) writeFileSync(manifestPath, manifestContent);
290
- const patchPath = join(profileDir, PROFILE_PATCH_FILENAME);
291
- const patchContent = readFileSync(join(bundleDir, HEADLESS_PATCH_FILENAME), "utf8") + presetRosterLayer(config.preset) + permissionBoundaryLayer(config.permissions);
292
- if (!existsSync(patchPath) || readFileSync(patchPath, "utf8") !== patchContent) writeFileSync(patchPath, patchContent);
293
- const bundleLink = join(profileDir, "node_modules", "@khorsheed", "dsh-local-agent-dsh-headless");
294
- mkdirSync(dirname(bundleLink), { recursive: true });
295
- ensureSymlink(bundleLink, bundleDir);
346
+ const hostBundleDir = resolveHeadlessBundleDir(config);
347
+ const unitDir = config.unitHeadlessBundleDir;
348
+ const profileDir = provisionOneSubProfile(homeDir, profileName, hostBundleDir, hostBundleDir, config, config.headlessBundleDir !== void 0 && unitDir === void 0 ? "link" : "copy");
349
+ if (unitDir !== void 0) {
350
+ const patchSource = existsSync(join(unitDir, HEADLESS_PATCH_FILENAME)) ? unitDir : hostBundleDir;
351
+ provisionOneSubProfile(homeDir, unitSubProfileName(config), unitDir, patchSource, config, "link");
352
+ }
296
353
  return profileDir;
297
354
  }
298
355
  /**
299
- * The preset root a harness home derives — the roster's own rule, spelled
300
- * here because this module has to write into it.
356
+ * Where a scope keeps its own copy of each preset PACKAGE it composes:
357
+ * `<scoped home>/preset-packages/<id>`. (0.1.x used `.agent-presets/<id>`,
358
+ * the directory roster's derived user root; 0.2.0 has no directory scan.)
301
359
  */
302
- const USER_PRESET_DIR = ".agent-presets";
303
- /** The file that makes a preset directory a preset (the roster's rule). */
304
- const PRESET_COMPOSITION_FILE = "agent.cordis.yml";
360
+ const SCOPE_PRESET_PACKAGES_DIR = "preset-packages";
361
+ /**
362
+ * The scope's copy of one preset package.
363
+ * @param homeDir - the harness scoped home.
364
+ * @param id - the preset id.
365
+ * @returns the directory, whether or not it exists.
366
+ */
367
+ function scopePresetPackageDir(homeDir, id) {
368
+ return join(homeDir, SCOPE_PRESET_PACKAGES_DIR, id);
369
+ }
305
370
  /**
306
371
  * The scope's own declaration of what its sub-profile composes.
307
372
  *
308
373
  * A FILE in the scope directory, for the third time and the same reason as
309
- * the roster layer and the permission boundary: a scoped home bind-mounted
374
+ * the preset layer and the permission boundary: a scoped home bind-mounted
310
375
  * into an evaluation unit carries its own composition with it, and nothing
311
376
  * has to ride the spawn env or be baked into an image.
312
377
  *
313
378
  * It exists because the plugin's own `config` is instance-global — one
314
379
  * deployment, one answer — while a preset is a property of the SUBJECT. Two
315
- * scopes rostering two presets was therefore unexpressible, and the only way
316
- * to build it was to hand-append the roster layer to each scope's patch. That
380
+ * scopes composing two presets was therefore unexpressible, and the only way
381
+ * to build it was to hand-append the preset layer to each scope's patch. That
317
382
  * hand edit does not survive: this module regenerates the patch WHOLE, and
318
383
  * the registry re-provisions a scope the first time anything names it in a
319
- * fresh host process — so the roster silently disappears on the next restart,
320
- * and the next read-back reports a scope that rosters nothing.
384
+ * fresh host process — so the layer silently disappears on the next restart,
385
+ * and the next read-back reports a scope that composes nothing.
321
386
  */
322
387
  const SCOPE_SUB_PROFILE_FILENAME = "sub-profile.json";
323
388
  /**
@@ -381,32 +446,30 @@ const EXPRESSION_SCHEMA = yaml.DEFAULT_SCHEMA.extend([
381
446
  /** A quoted absolute path inside an expression's source text. */
382
447
  const QUOTED_ABSOLUTE_RE = /(['"])(\/[^'"\n]*)\1/g;
383
448
  /**
384
- * Every absolute filesystem path a preset composition names.
449
+ * Every absolute filesystem path a preset bundle patch names.
385
450
  *
386
451
  * An absolute path is the one thing in a composition that cannot be true in
387
452
  * two places at once, and a preset used as an evaluation factor is copied:
388
- * it is read on the host from the deployment's preset root, again from the
453
+ * it is read on the host from the instance's installation, again from the
389
454
  * scope's own copy, and a third time inside a unit where the scope is bound
390
455
  * at a different mount point. `skill-filesystem` resolves each
391
456
  * `customSkillDirs` entry with `resolve()`, so an absolute entry is taken as
392
457
  * written and a bare relative one resolves against the process's cwd —
393
458
  * neither travels.
394
459
  *
395
- * What does travel is the loader's own expression form, which the shipped
396
- * `cordis` preset already uses:
460
+ * What does travel is resolving the package's own directory through the
461
+ * loader's `baseUrl` (the declaring profile's root), the form the official
462
+ * web-app presets use:
397
463
  *
398
464
  * ```yaml
399
465
  * customSkillDirs:
400
- * - !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('skills/', baseUrl))"
466
+ * - !!js "process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:path').dirname(process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('<pkg>/package.json')), 'skills')"
401
467
  * ```
402
468
  *
403
- * `baseUrl` is the composition's own directory (the include rewrites it), so
404
- * the root resolves wherever the preset is installed.
405
- *
406
469
  * Unparsable YAML reports NO findings: the loader is the authority on whether
407
470
  * a composition loads, and refusing a preset because this reader could not
408
471
  * parse it would be this module overruling it.
409
- * @param text - the `agent.cordis.yml` content.
472
+ * @param text - the bundle patch content.
410
473
  * @returns the offending literals, in document order, deduplicated.
411
474
  */
412
475
  function compositionAbsolutePaths(text) {
@@ -443,7 +506,7 @@ function compositionAbsolutePaths(text) {
443
506
  walk(document);
444
507
  return found;
445
508
  }
446
- /** Every regular file under `dir`, as posix-ish relative paths, sorted. */
509
+ /** Every regular file under `dir`, as posix-ish relative paths, sorted (a top-level `node_modules` is not part of a preset). */
447
510
  function presetTreeFiles(dir) {
448
511
  const files = [];
449
512
  const walk = (absDir, rel) => {
@@ -454,6 +517,7 @@ function presetTreeFiles(dir) {
454
517
  return false;
455
518
  }
456
519
  for (const entry of entries) {
520
+ if (rel === "" && entry.name === "node_modules") continue;
457
521
  const relChild = rel === "" ? entry.name : `${rel}/${entry.name}`;
458
522
  if (entry.isSymbolicLink()) return false;
459
523
  if (entry.isDirectory()) {
@@ -469,12 +533,13 @@ function presetTreeFiles(dir) {
469
533
  return files.sort();
470
534
  }
471
535
  /**
472
- * Whether two preset directories are the same directory, byte for byte.
536
+ * Whether two preset package directories are the same directory, byte for
537
+ * byte.
473
538
  *
474
539
  * This is the whole claim the evaluation rests on: a capability face carries
475
540
  * no filesystem path, so the fingerprint taken against the deployment's copy
476
541
  * describes the scope's copy exactly when the two hold the same bytes.
477
- * @param a - one preset directory.
542
+ * @param a - one preset package directory.
478
543
  * @param b - the other.
479
544
  * @returns true when both hold the same files with the same contents.
480
545
  */
@@ -491,10 +556,10 @@ function presetTreesEqual(a, b) {
491
556
  }
492
557
  });
493
558
  }
494
- /** Copy one preset directory onto another, replacing whatever was there. */
559
+ /** Copy one preset package directory onto another, replacing whatever was there. */
495
560
  function mirrorPresetTree(source, target) {
496
561
  const files = presetTreeFiles(source);
497
- if (files === void 0) throw new Error(`provision-dsh: preset directory ${source} is not a plain tree of files (a symlink or a device node cannot be copied into a scope — an evaluation unit resolves neither)`);
562
+ if (files === void 0) throw new Error(`provision-dsh: preset package ${source} is not a plain tree of files (a symlink or a device node cannot be copied into a scope — an evaluation unit resolves neither)`);
498
563
  rmSync(target, {
499
564
  recursive: true,
500
565
  force: true
@@ -505,82 +570,189 @@ function mirrorPresetTree(source, target) {
505
570
  copyFileSync(join(source, name), destination);
506
571
  }
507
572
  }
573
+ /** A JSON object read off disk, or undefined. */
574
+ function readJsonObject(path) {
575
+ try {
576
+ const value = JSON.parse(readFileSync(path, "utf8"));
577
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
578
+ } catch {
579
+ return;
580
+ }
581
+ }
582
+ /** The bundle patch files a package manifest declares (`dsh.bundle.patch`, a string or a list). */
583
+ function bundlePatchFiles(manifest) {
584
+ const dsh = manifest["dsh"];
585
+ if (typeof dsh !== "object" || dsh === null) return [];
586
+ const bundle = dsh["bundle"];
587
+ if (typeof bundle !== "object" || bundle === null) return [];
588
+ const patch = bundle["patch"];
589
+ if (typeof patch === "string") return [patch];
590
+ return Array.isArray(patch) ? patch.filter((item) => typeof item === "string") : [];
591
+ }
592
+ /** The preset ids a bundle patch document declares through `@deepseek-ai/dsh-agent-preset` rows. */
593
+ function declaredPresetIds(text) {
594
+ let document;
595
+ try {
596
+ document = yaml.load(text, { schema: EXPRESSION_SCHEMA });
597
+ } catch {
598
+ return [];
599
+ }
600
+ if (!Array.isArray(document)) return [];
601
+ const ids = [];
602
+ const visit = (row) => {
603
+ if (typeof row !== "object" || row === null || Array.isArray(row)) return;
604
+ const record = row;
605
+ if (record["name"] !== "@deepseek-ai/dsh-agent-preset") return;
606
+ const config = record["config"];
607
+ if (typeof config !== "object" || config === null) return;
608
+ const id = config["id"];
609
+ if (typeof id === "string") ids.push(id);
610
+ };
611
+ for (const operation of document) {
612
+ if (typeof operation !== "object" || operation === null) continue;
613
+ const insert = operation["insert"];
614
+ if (Array.isArray(insert)) insert.forEach(visit);
615
+ else visit(operation);
616
+ }
617
+ return ids;
618
+ }
619
+ /**
620
+ * Find the bundle package, among a profile's direct dependencies, whose
621
+ * bundle patch declares preset `id`.
622
+ *
623
+ * This is how the 0.2.0 host itself learns the preset: a profile's direct
624
+ * `dsh.bundle`-declaring dependency mounts its patch, and the patch's
625
+ * `@deepseek-ai/dsh-agent-preset` row registers the id. Reading the same files
626
+ * is what lets a scope copy exactly the package the evaluation instance's
627
+ * capability catalog measures.
628
+ * @param profileDir - the instance profile directory (holding `package.json`).
629
+ * @param id - the preset id.
630
+ * @returns the declaring package, or undefined when none (or more than one) declares it.
631
+ */
632
+ function findPresetPackage(profileDir, id) {
633
+ const manifest = readJsonObject(join(profileDir, "package.json"));
634
+ if (manifest === void 0) return void 0;
635
+ const names = /* @__PURE__ */ new Set();
636
+ for (const field of [
637
+ "dependencies",
638
+ "optionalDependencies",
639
+ "devDependencies"
640
+ ]) {
641
+ const deps = manifest[field];
642
+ if (typeof deps === "object" && deps !== null) for (const name of Object.keys(deps)) names.add(name);
643
+ }
644
+ const found = [];
645
+ for (const name of [...names].sort()) {
646
+ if (!PACKAGE_NAME_RE.test(name)) continue;
647
+ let dir;
648
+ try {
649
+ dir = realpathSync(join(profileDir, "node_modules", ...name.split("/")));
650
+ } catch {
651
+ continue;
652
+ }
653
+ const packageManifest = readJsonObject(join(dir, "package.json"));
654
+ if (packageManifest === void 0) continue;
655
+ if (bundlePatchFiles(packageManifest).some((file) => {
656
+ try {
657
+ return declaredPresetIds(readFileSync(join(dir, file), "utf8")).includes(id);
658
+ } catch {
659
+ return false;
660
+ }
661
+ })) found.push({
662
+ name,
663
+ dir
664
+ });
665
+ }
666
+ return found.length === 1 ? found[0] : void 0;
667
+ }
668
+ /**
669
+ * Why a preset package directory cannot be copied into a scope, or undefined
670
+ * when it can: it must name itself, declare its bundle patch, and the patch
671
+ * must declare the preset without naming an absolute path.
672
+ */
673
+ function presetPackageProblem(dir, id) {
674
+ const manifest = readJsonObject(join(dir, "package.json"));
675
+ const name = manifest?.["name"];
676
+ if (manifest === void 0 || typeof name !== "string" || !PACKAGE_NAME_RE.test(name)) return `${dir} has no package.json naming the package`;
677
+ const files = bundlePatchFiles(manifest);
678
+ if (files.length === 0) return `${name} declares no dsh.bundle.patch`;
679
+ let declares = false;
680
+ for (const file of files) {
681
+ let text;
682
+ try {
683
+ text = readFileSync(join(dir, file), "utf8");
684
+ } catch {
685
+ return `${name} declares bundle patch ${file}, which is missing`;
686
+ }
687
+ const offenders = compositionAbsolutePaths(text);
688
+ if (offenders.length > 0) return `${name}'s ${file} names absolute path(s) ${offenders.map((path) => JSON.stringify(path)).join(", ")} — a preset that is copied into a scope and bind-mounted into a unit is read from three different directories, so an absolute path is wrong in at least two of them. Resolve the package's own directory through baseUrl instead: !!js "…dirname(createRequire(baseUrl).resolve('${name}/package.json'))…"`;
689
+ if (declaredPresetIds(text).includes(id)) declares = true;
690
+ }
691
+ return declares ? void 0 : `${name} declares no ${PRESET_ROW_MODULE} row with id ${JSON.stringify(id)}`;
692
+ }
508
693
  /**
509
- * Give one scope its own copy of a preset, from the deployment's preset root.
694
+ * Give one scope its own copy of a preset package, from the evaluation
695
+ * instance's own installation of it.
510
696
  *
511
- * The copy is what makes a preset reachable on BOTH paths. The roster derives
512
- * a user root of `<$DSH_HOME>/.agent-presets`, and the sub-dsh runs with
513
- * `DSH_HOME` pointed at the scoped home — which is `<scope>/.agent-presets`
514
- * on the host and `/creds/dsh/.agent-presets` inside an evaluation unit,
515
- * one directory, because the unit binds the scope. A roster pointed at the
516
- * deployment's root instead resolves on the host and names a path the unit
517
- * does not have.
697
+ * The copy is what makes a preset reachable on BOTH paths. The sub-profile
698
+ * resolves the package through a relative link to
699
+ * `<scope>/preset-packages/<id>`, which is one directory on the host and
700
+ * inside an evaluation unit, because the unit binds the scope.
518
701
  *
519
702
  * `refresh` is the difference between provisioning a condition and healing a
520
- * scope. A deliberate provision re-syncs from the deployment's copy, because
703
+ * scope. A deliberate provision re-syncs from the instance's copy, because
521
704
  * that is the moment a new lock is minted and an edited preset should be
522
705
  * picked up. Every other provisioning leaves an existing copy alone: nothing
523
- * may move the subject under a run.
706
+ * may move the subject under a run — and a healing call whose instance no
707
+ * longer carries the package keeps the copy, reporting `matchesSource: false`.
524
708
  * @param homeDir - the harness scoped home.
525
- * @param presetRoot - the deployment's preset root.
526
- * @param id - the preset id (a directory name in both roots).
709
+ * @param instanceProfileDir - the instance profile directory, or undefined when unknown.
710
+ * @param id - the preset id.
527
711
  * @param options - `refresh` re-syncs an existing copy.
528
712
  * @returns where the copy is and whether it matches the source.
529
- * @throws when the source is missing, is not a preset, or names an absolute path.
713
+ * @throws when no copy can be made: no installed package declares the id, or it is not a usable preset package.
530
714
  */
531
- function snapshotScopePreset(homeDir, presetRoot, id, options = {}) {
715
+ function snapshotScopePreset(homeDir, instanceProfileDir, id, options = {}) {
532
716
  if (!SUB_PROFILE_PRESET_ID_RE.test(id)) throw new Error(`provision-dsh: preset id ${JSON.stringify(id)} must match ${String(SUB_PROFILE_PRESET_ID_RE)} (it is a directory name)`);
533
- const source = join(presetRoot, id);
534
- const composition = join(source, PRESET_COMPOSITION_FILE);
535
- if (!existsSync(composition)) throw new Error(`provision-dsh: no preset ${JSON.stringify(id)} in ${presetRoot} (expected ${join(id, PRESET_COMPOSITION_FILE)}) — install it into this deployment's preset root before a scope can compose it`);
536
- const offenders = compositionAbsolutePaths(readFileSync(composition, "utf8"));
537
- if (offenders.length > 0) throw new Error(`provision-dsh: preset ${JSON.stringify(id)} names absolute path(s) ${offenders.map((path) => JSON.stringify(path)).join(", ")} — a preset that is copied into a scope and bind-mounted into a unit is read from three different directories, so an absolute path is wrong in at least two of them. Use the loader expression the shipped \`cordis\` preset uses: !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('skills/', baseUrl))"`);
538
- const dir = join(homeDir, USER_PRESET_DIR, id);
539
- if (existsSync(dir) && options.refresh !== true) return {
717
+ const dir = scopePresetPackageDir(homeDir, id);
718
+ const copyName = () => {
719
+ const name = readJsonObject(join(dir, "package.json"))?.["name"];
720
+ return typeof name === "string" && PACKAGE_NAME_RE.test(name) ? name : void 0;
721
+ };
722
+ const source = instanceProfileDir === void 0 ? void 0 : findPresetPackage(instanceProfileDir, id);
723
+ const existing = existsSync(dir) ? copyName() : void 0;
724
+ if (existing !== void 0 && options.refresh !== true) return {
540
725
  dir,
541
- matchesSource: presetTreesEqual(source, dir),
726
+ packageName: existing,
727
+ matchesSource: source !== void 0 && presetTreesEqual(source.dir, dir),
542
728
  copied: false
543
729
  };
544
- if (existsSync(dir) && presetTreesEqual(source, dir)) return {
730
+ if (source === void 0) throw new Error(`provision-dsh: no package installed in ${instanceProfileDir ?? "this instance's profile"} declares preset ${JSON.stringify(id)} (an ${PRESET_ROW_MODULE} row in its dsh.bundle.patch) — add the preset package to the instance profile before a scope can compose it`);
731
+ const problem = presetPackageProblem(source.dir, id);
732
+ if (problem !== void 0) throw new Error(`provision-dsh: preset ${JSON.stringify(id)}: ${problem}`);
733
+ if (existing !== void 0 && presetTreesEqual(source.dir, dir)) return {
545
734
  dir,
735
+ packageName: existing,
546
736
  matchesSource: true,
547
737
  copied: false
548
738
  };
549
- mirrorPresetTree(source, dir);
739
+ mirrorPresetTree(source.dir, dir);
550
740
  return {
551
741
  dir,
742
+ packageName: source.name,
552
743
  matchesSource: true,
553
744
  copied: true
554
745
  };
555
746
  }
556
747
  /**
557
- * The deployment's own preset root, when the caller names no other.
558
- *
559
- * The roster's own derivation, repeated here because the provisioning paths
560
- * that heal a scope (a host round, a live runtime) have no settings service
561
- * in reach: a non-blank `$DSH_HOME`, else the `~/.dsh` installation default
562
- * (the precedence of `resolveDshHome` in `@deepseek-ai/dsh-home-paths` —
563
- * the default was once hardcoded to `.dsh-official`, pointing a desktop
564
- * instance at another instance's home; 2026-09-30).
565
- */
566
- function defaultPresetRoot() {
567
- const fromEnv = process.env["DSH_HOME"];
568
- const home = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(homedir(), ".dsh");
569
- return join(home, USER_PRESET_DIR);
570
- }
571
- /**
572
- * Provision one scope: its own preset copy, its declaration, and the
573
- * sub-profile that rosters it.
748
+ * Provision one scope: its own preset package copy, its declaration, and the
749
+ * sub-profile that composes it.
574
750
  *
575
751
  * The preset resolves in one order, and the order is the decision: the
576
752
  * CALLER's explicit request, then the SCOPE's own declaration, then the
577
753
  * deployment's plugin config. The first is how a condition says which subject
578
754
  * this scope is; the second is how that survives a restart; the third is the
579
755
  * deployment-wide answer every scope had before either existed.
580
- *
581
- * A scope-owned preset rosters with NO `roots`. The roster's derived user
582
- * root is exactly the scope's copy on both paths, so leaving the roots alone
583
- * is what gives the host and the unit one resolution rule instead of two.
584
756
  * @param homeDir - the harness scoped home.
585
757
  * @param config - the deployment's sub-profile config.
586
758
  * @param options - per-scope inputs.
@@ -589,10 +761,13 @@ function defaultPresetRoot() {
589
761
  function provisionDshScope(homeDir, config = {}, options = {}) {
590
762
  const requested = options.preset;
591
763
  const declared = requested ?? readScopeSubProfile(homeDir)?.preset;
592
- if (requested !== void 0) writeScopeSubProfile(homeDir, { preset: requested });
593
764
  let snapshot;
594
- if (declared !== void 0) snapshot = snapshotScopePreset(homeDir, options.presetRoot ?? defaultPresetRoot(), declared, { refresh: requested !== void 0 });
595
- const preset = declared === void 0 ? config.preset : { id: declared };
765
+ if (declared !== void 0) snapshot = snapshotScopePreset(homeDir, options.instanceProfileDir, declared, { refresh: requested !== void 0 });
766
+ if (requested !== void 0) writeScopeSubProfile(homeDir, { preset: requested });
767
+ const preset = declared === void 0 || snapshot === void 0 ? config.preset : {
768
+ id: declared,
769
+ package: snapshot.packageName
770
+ };
596
771
  return {
597
772
  profileDir: provisionDshSubProfile(homeDir, {
598
773
  ...config,
@@ -603,4 +778,4 @@ function provisionDshScope(homeDir, config = {}, options = {}) {
603
778
  };
604
779
  }
605
780
  //#endregion
606
- export { DEFAULT_SUB_PROFILE_NAME, PRESET_ROSTER_MODULE, SCOPE_SUB_PROFILE_FILENAME, SUB_PROFILE_PRESET_ID_RE, USER_PRESET_DIR, compositionAbsolutePaths, defaultPresetRoot, permissionBoundaryLayer, presetRosterLayer, presetTreesEqual, provisionDshScope, provisionDshSubProfile, readScopeSubProfile, readSubProfilePermissions, readSubProfilePreset, resolveHeadlessBundleDir, snapshotScopePreset, writeScopeSubProfile };
781
+ export { DEFAULT_SUB_PROFILE_NAME, PRESET_REGISTRY_MODULE, PRESET_ROW_MODULE, SCOPE_PRESET_PACKAGES_DIR, SCOPE_SUB_PROFILE_FILENAME, SUB_PROFILE_PRESET_ID_RE, UNIT_PROFILE_SUFFIX, compositionAbsolutePaths, findPresetPackage, permissionBoundaryLayer, presetRegistryLayer, presetTreesEqual, provisionDshScope, provisionDshSubProfile, readScopeSubProfile, readSubProfilePermissions, readSubProfilePreset, resolveHeadlessBundleDir, scopePresetPackageDir, snapshotScopePreset, unitSubProfileName, writeScopeSubProfile };