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