@deepseek-ai/dsh-agent-presets 0.0.1-rc.1

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.
@@ -0,0 +1,1123 @@
1
+ import { chmod, cp, readFile, readdir, rm, stat } from "node:fs/promises";
2
+ import { Context, Service } from "@deepseek-ai/cordis";
3
+ import z from "@deepseek-ai/schemastery";
4
+ import { bindScopeParent, createScope, scopeOf, scopeParentOf } from "@deepseek-ai/dsh-scope";
5
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
6
+ import { dirname, isAbsolute, join, resolve } from "node:path";
7
+ import yaml, { load } from "js-yaml";
8
+ import { Include, entryListSchema } from "@deepseek-ai/cordis-plugin-include";
9
+ import { expandHomePath } from "@deepseek-ai/dsh-paths";
10
+ import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
11
+ import { pathToFileURL } from "node:url";
12
+ //#region src/metadata.ts
13
+ /**
14
+ * A preset's display metadata: the name and description a picker shows.
15
+ *
16
+ * It lives in its own file because the composition is a top-level list of
17
+ * plugin rows — YAML cannot carry sibling keys beside it, and faking a
18
+ * metadata row would hand the Loader something to load. Keeping it separate
19
+ * also keeps the composition exactly what its name says: a Cordis file the
20
+ * loader owns and the cordis preset can author.
21
+ *
22
+ * The file carries display text ONLY. `id` is the directory name and `trust`
23
+ * comes from the root a preset was discovered under, so neither is writable
24
+ * here — otherwise a locally authored preset could claim to be a shipped one.
25
+ *
26
+ * Every read failure degrades to no metadata. A preset whose display text is
27
+ * missing, malformed, or unreadable still mounts: presentation is not a
28
+ * capability, and a broken name must never become an agent that cannot start.
29
+ * @module @deepseek-ai/dsh-agent-presets/metadata
30
+ */
31
+ /** The optional display-metadata file beside a preset's composition. */
32
+ const METADATA_FILE = "preset.yml";
33
+ /** A non-empty trimmed string, or undefined for anything else. */
34
+ function text(value) {
35
+ if (typeof value !== "string") return void 0;
36
+ const trimmed = value.trim();
37
+ return trimmed === "" ? void 0 : trimmed;
38
+ }
39
+ /**
40
+ * Read one preset directory's display metadata.
41
+ *
42
+ * Absent, unparsable, and wrongly-shaped files are all the same answer —
43
+ * empty metadata — because the caller renders a picker, not a diagnostic.
44
+ * @param directory - the preset directory.
45
+ * @returns the display text the preset published, possibly empty.
46
+ */
47
+ async function readPresetMetadata(directory) {
48
+ let raw;
49
+ try {
50
+ raw = await readFile(join(directory, METADATA_FILE), "utf8");
51
+ } catch {
52
+ return {};
53
+ }
54
+ let parsed;
55
+ try {
56
+ parsed = yaml.load(raw);
57
+ } catch {
58
+ return {};
59
+ }
60
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
61
+ const record = parsed;
62
+ const name = text(record.name);
63
+ const description = text(record.description);
64
+ const order = typeof record.order === "number" && Number.isFinite(record.order) ? record.order : void 0;
65
+ return {
66
+ ...name === void 0 ? {} : { name },
67
+ ...description === void 0 ? {} : { description },
68
+ ...order === void 0 ? {} : { order }
69
+ };
70
+ }
71
+ /**
72
+ * Render display metadata as the file's contents.
73
+ *
74
+ * Absent fields are omitted rather than written empty, so a preset with no
75
+ * description does not ship a key that reads as an intentional blank.
76
+ * @param metadata - the display text to store.
77
+ * @returns the YAML document, or undefined when there is nothing to store.
78
+ */
79
+ function renderPresetMetadata(metadata) {
80
+ const name = text(metadata.name);
81
+ const description = text(metadata.description);
82
+ const { order } = metadata;
83
+ if (name === void 0 && description === void 0 && order === void 0) return void 0;
84
+ return yaml.dump({
85
+ ...name === void 0 ? {} : { name },
86
+ ...description === void 0 ? {} : { description },
87
+ ...order === void 0 ? {} : { order }
88
+ }, { lineWidth: -1 });
89
+ }
90
+ //#endregion
91
+ //#region src/types.ts
92
+ /**
93
+ * Ids a preset directory may use.
94
+ *
95
+ * The id becomes a path segment, so this is a containment boundary rather than
96
+ * a style rule: `..`, a separator, or an absolute-looking name would place the
97
+ * composition outside the root the deployment authorised. Discovery shares it:
98
+ * a directory whose name no copy could ever claim is not a preset slot.
99
+ */
100
+ const PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
101
+ /**
102
+ * No configured root supplies the requested preset.
103
+ *
104
+ * Separate from a mount failure because the two mean different things to a
105
+ * caller: an unknown id is a bad request, while an unusable composition is a
106
+ * broken preset the deployment must fix.
107
+ */
108
+ var UnknownPresetError = class extends Error {
109
+ presetId;
110
+ available;
111
+ constructor(presetId, available) {
112
+ super(`agent-presets: preset "${presetId}" not found (available: ${available.join(", ") || "none"})`);
113
+ this.presetId = presetId;
114
+ this.available = available;
115
+ }
116
+ };
117
+ /** A preset exists but its composition cannot be installed. */
118
+ var PresetMountError = class extends Error {
119
+ presetId;
120
+ reason;
121
+ constructor(presetId, reason, options) {
122
+ super(`agent-presets: preset "${presetId}" failed to mount: ${reason}`, options);
123
+ this.presetId = presetId;
124
+ this.reason = reason;
125
+ }
126
+ };
127
+ //#endregion
128
+ //#region src/discovery.ts
129
+ /**
130
+ * Filesystem discovery of agent presets. A preset is a directory holding
131
+ * {@link COMPOSITION_FILE}, optionally beside a {@link METADATA_FILE} carrying
132
+ * its display text; the directory name is the preset id. Discovery
133
+ * re-reads the roots on every call so a preset authored while the process is
134
+ * running is visible without a restart.
135
+ *
136
+ * Discovery also owns preset HEALTH: a directory whose composition is
137
+ * missing or unloadable is reported as a broken roster row rather than
138
+ * skipped. A skipped directory would still occupy its id on disk — the copy
139
+ * path refuses the name while no surface shows anything to delete — and a
140
+ * malformed composition would otherwise read as an ordinary preset until the
141
+ * first session fails to mount it.
142
+ * @module @deepseek-ai/dsh-agent-presets/discovery
143
+ */
144
+ /** The composition file that makes a directory a preset. */
145
+ const COMPOSITION_FILE = "agent.cordis.yml";
146
+ /**
147
+ * Why `rows` cannot be an entry list, or undefined when it can.
148
+ *
149
+ * A shallow shape check, deliberately short of the loader's work: it does not
150
+ * resolve plugin names or apply configs. What it catches is the hand-edit
151
+ * that produces a file the loader cannot even begin with — and it must accept
152
+ * everything the loader accepts, which is why rows are only required to be
153
+ * maps carrying a plugin `name` (groups recurse into their own lists).
154
+ * @param rows - the parsed composition document.
155
+ * @param at - row-path prefix for nested diagnostics, empty at the top level.
156
+ * @returns one human-readable reason, or undefined when the shape holds.
157
+ */
158
+ function entryListProblem(rows, at = "") {
159
+ if (!Array.isArray(rows)) return at === "" ? "the composition must be a top-level list of plugin rows" : `group ${at} must hold a list of plugin rows`;
160
+ for (const [index, row] of rows.entries()) {
161
+ const label = at === "" ? `row ${String(index + 1)}` : `${at} row ${String(index + 1)}`;
162
+ if (typeof row !== "object" || row === null || Array.isArray(row)) return `${label} is not a plugin row (expected a map with a "name")`;
163
+ const { name, group, config } = row;
164
+ if (typeof name !== "string" || name === "") return `${label} names no plugin (a "name" string is required)`;
165
+ if (group === true) {
166
+ const nested = entryListProblem(config, label);
167
+ if (nested !== void 0) return nested;
168
+ }
169
+ }
170
+ }
171
+ /**
172
+ * Why the composition at `path` cannot mount, or undefined when it looks
173
+ * loadable. Parsed with the loader's own YAML dialect ({@link entryListSchema},
174
+ * the one carrying `!!js`), so health can never call a composition broken
175
+ * that the loader would accept.
176
+ * @param path - absolute path of the composition file.
177
+ * @returns one human-readable reason, or undefined when the file is loadable.
178
+ */
179
+ async function compositionProblem(path) {
180
+ let content;
181
+ try {
182
+ content = await readFile(path, "utf8");
183
+ } catch {
184
+ return `the composition file ${COMPOSITION_FILE} cannot be read`;
185
+ }
186
+ let rows;
187
+ try {
188
+ rows = load(content, { schema: entryListSchema });
189
+ } catch (error) {
190
+ return `the composition is not valid YAML: ${(error instanceof Error ? error.message : String(error)).replace(/\n[\s\S]*$/, "")}`;
191
+ }
192
+ return entryListProblem(rows);
193
+ }
194
+ /**
195
+ * Whether `path` names an existing regular file.
196
+ * @param path - absolute path to test.
197
+ * @returns true when the path resolves to a file.
198
+ */
199
+ async function isFile(path) {
200
+ try {
201
+ return (await stat(path)).isFile();
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+ /**
207
+ * Scan one root for preset directories.
208
+ *
209
+ * An absent root yields no presets rather than throwing: the user root does
210
+ * not exist until the first locally authored preset, and naming a default
211
+ * that no root supplies already fails loud at resolution.
212
+ *
213
+ * Every directory whose name is a usable preset id is a roster row — broken
214
+ * when its composition is missing or unloadable. A directory named outside
215
+ * {@link PRESET_ID} is skipped instead: no copy could ever claim that name,
216
+ * so it blocks nothing, and reporting `.DS_Store`-grade residue as broken
217
+ * presets would teach users to ignore the marker.
218
+ * @param root - the directory and the trust its presets inherit.
219
+ * @returns the root's presets ordered by id.
220
+ */
221
+ async function scanRoot(root) {
222
+ const dir = resolve(expandHomePath(root.path));
223
+ let children;
224
+ try {
225
+ children = await readdir(dir, { withFileTypes: true });
226
+ } catch (error) {
227
+ if (error.code === "ENOENT") return [];
228
+ throw new Error(`agent-presets: cannot read preset root ${dir}: ${String(error)}`, { cause: error });
229
+ }
230
+ const found = [];
231
+ for (const child of children) {
232
+ if (!child.isDirectory() || !PRESET_ID.test(child.name)) continue;
233
+ const directory = join(dir, child.name);
234
+ const path = join(directory, COMPOSITION_FILE);
235
+ const broken = await isFile(path) ? await compositionProblem(path) : `the composition file ${COMPOSITION_FILE} is missing — the directory still occupies the id; delete it or restore the file`;
236
+ const metadata = await readPresetMetadata(directory);
237
+ found.push({
238
+ id: child.name,
239
+ trust: root.trust,
240
+ path,
241
+ ...metadata,
242
+ ...broken === void 0 ? {} : { broken }
243
+ });
244
+ }
245
+ return found.sort((left, right) => {
246
+ const byOrder = (left.order ?? Number.POSITIVE_INFINITY) - (right.order ?? Number.POSITIVE_INFINITY);
247
+ return byOrder === 0 ? left.id.localeCompare(right.id) : byOrder;
248
+ });
249
+ }
250
+ /**
251
+ * Scan every root in precedence order.
252
+ * @param roots - roots in precedence order; an earlier root wins a duplicate id.
253
+ * @returns every discovered preset, first-root-wins per id.
254
+ */
255
+ async function discoverPresets(roots) {
256
+ const byId = /* @__PURE__ */ new Map();
257
+ for (const root of roots) for (const preset of await scanRoot(root)) {
258
+ if (byId.has(preset.id)) continue;
259
+ byId.set(preset.id, preset);
260
+ }
261
+ return [...byId.values()];
262
+ }
263
+ //#endregion
264
+ //#region src/authoring.ts
265
+ /**
266
+ * Copying, reading, and deleting locally authored presets.
267
+ *
268
+ * Authoring is confined to a `user` root: the shipped `.system` set is part of
269
+ * the deployment, and letting a browser rewrite it would turn "reset to a known
270
+ * preset" into something the same caller could have broken first.
271
+ *
272
+ * The only authoring write is a whole-directory copy of an existing preset.
273
+ * No caller supplies composition text: the inputs are ids the host resolves
274
+ * against its own roots plus an optional display name, so authoring grants no
275
+ * capability the copied preset did not already carry.
276
+ * @module @deepseek-ai/dsh-agent-presets/authoring
277
+ */
278
+ /** A preset id that cannot be used as a directory name under a root. */
279
+ var InvalidPresetIdError = class extends Error {
280
+ presetId;
281
+ constructor(presetId) {
282
+ super(`agent-presets: preset id ${JSON.stringify(presetId)} must match ${String(PRESET_ID)} — the id is a directory name, so anything else could escape the preset root`);
283
+ this.presetId = presetId;
284
+ }
285
+ };
286
+ /** A copy target that is already occupied — a copy never overwrites. */
287
+ var PresetExistsError = class extends Error {
288
+ presetId;
289
+ constructor(presetId) {
290
+ super(`agent-presets: preset "${presetId}" already exists — a copy never overwrites; delete the existing preset first or choose another id`);
291
+ this.presetId = presetId;
292
+ }
293
+ };
294
+ /** Authoring was attempted where the deployment allows none. */
295
+ var PresetNotWritableError = class extends Error {
296
+ presetId;
297
+ constructor(presetId, reason) {
298
+ super(`agent-presets: preset "${presetId}" cannot be written: ${reason}`);
299
+ this.presetId = presetId;
300
+ }
301
+ };
302
+ /**
303
+ * The root locally authored presets are written to.
304
+ * @param roots - the configured roots in precedence order.
305
+ * @returns the absolute path of the first `user` root.
306
+ * @throws when the deployment configured no writable root.
307
+ */
308
+ function writableRoot(roots) {
309
+ const root = roots.find((candidate) => candidate.trust === "user");
310
+ if (root === void 0) throw new PresetNotWritableError("", "this deployment configures no user-writable preset root");
311
+ return resolve(expandHomePath(root.path));
312
+ }
313
+ /**
314
+ * Read one preset's composition text.
315
+ * @param preset - the resolved preset.
316
+ * @returns the file's contents.
317
+ */
318
+ async function readComposition(preset) {
319
+ return await readFile(preset.path, "utf8");
320
+ }
321
+ /** Whether anything occupies the path (cp's own errorOnExist backstops races). */
322
+ async function occupied(path) {
323
+ let present = true;
324
+ try {
325
+ await stat(path);
326
+ } catch {
327
+ present = false;
328
+ }
329
+ return present;
330
+ }
331
+ /**
332
+ * Re-tighten a copied tree to owner-only. A shipped preset is world-readable
333
+ * in its install and `cp` preserves that; the copy carries the same weight as
334
+ * the settings document beside it, so group/other access is stripped. A
335
+ * file's owner-execute bit survives — a preset may ship runnable helpers.
336
+ */
337
+ async function tightenModes(dir) {
338
+ await chmod(dir, 448);
339
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
340
+ const target = join(dir, entry.name);
341
+ if (entry.isDirectory()) await tightenModes(target);
342
+ else
343
+ /* v8 ignore next -- Windows exposes no POSIX owner-execute bit; the POSIX lane covers both file modes. */
344
+ await chmod(target, ((await stat(target)).mode & 64) === 0 ? 384 : 448);
345
+ }
346
+ }
347
+ /**
348
+ * Create a preset by copying an existing one's whole directory.
349
+ *
350
+ * The copy carries everything the source directory holds — composition,
351
+ * metadata, skill directories, assets — because a preset is its directory,
352
+ * not one file. Symlinks are dereferenced so the copy is self-contained
353
+ * rather than a set of links back into the install it was copied from.
354
+ *
355
+ * The copied metadata is then rewritten: the source's description is kept
356
+ * (the file is the author's to edit afterwards), but its name and roster
357
+ * `order` are not — a copy presenting itself identically to its source, or
358
+ * sorted into the shipped set's declared order, would make the roster stop
359
+ * distinguishing them. With no name given and no description to keep, the
360
+ * file is removed so the copy publishes nothing rather than a blank.
361
+ * @param roots - the configured roots; the first `user` one receives the copy.
362
+ * @param source - the resolved preset the copy starts from.
363
+ * @param id - the new preset's id, which becomes its directory name.
364
+ * @param name - display name for the copy; omitted falls back to the id.
365
+ * @returns the absolute path of the new preset directory.
366
+ * @throws when the id is unusable or already occupied on disk, or the
367
+ * deployment configures no writable root.
368
+ */
369
+ async function copyComposition(roots, source, id, name) {
370
+ if (!PRESET_ID.test(id)) throw new InvalidPresetIdError(id);
371
+ const dir = join(writableRoot(roots), id);
372
+ if (await occupied(dir)) throw new PresetExistsError(id);
373
+ try {
374
+ await cp(dirname(source.path), dir, {
375
+ recursive: true,
376
+ dereference: true,
377
+ force: false,
378
+ errorOnExist: true
379
+ });
380
+ await tightenModes(dir);
381
+ const rendered = renderPresetMetadata({
382
+ ...name === void 0 ? {} : { name },
383
+ ...source.description === void 0 ? {} : { description: source.description }
384
+ });
385
+ const metadataPath = join(dir, METADATA_FILE);
386
+ if (rendered === void 0) await rm(metadataPath, { force: true });
387
+ else await writeFileAtomic(metadataPath, rendered, {
388
+ mode: 384,
389
+ dirMode: 448
390
+ });
391
+ } catch (error) {
392
+ await rm(dir, {
393
+ recursive: true,
394
+ force: true
395
+ });
396
+ throw error;
397
+ }
398
+ return dir;
399
+ }
400
+ /**
401
+ * Delete a locally authored preset.
402
+ *
403
+ * A shipped preset is refused: it belongs to the deployment. A preset a live
404
+ * session mounted is NOT refused — the composition was read at creation and is
405
+ * never re-read, so that session keeps running exactly as it was.
406
+ * @param roots - the configured roots.
407
+ * @param preset - the resolved preset to remove.
408
+ * @throws when the preset ships with the deployment or lies outside the writable root.
409
+ */
410
+ async function deleteComposition(roots, preset) {
411
+ if (preset.trust !== "user") throw new PresetNotWritableError(preset.id, "it ships with the deployment");
412
+ const dir = join(writableRoot(roots), preset.id);
413
+ if (!isAbsolute(preset.path) || !preset.path.startsWith(dir)) throw new PresetNotWritableError(preset.id, "it does not live under the writable preset root");
414
+ await rm(dir, {
415
+ recursive: true,
416
+ force: true
417
+ });
418
+ }
419
+ //#endregion
420
+ //#region src/mount.ts
421
+ /**
422
+ * Mount one preset composition under an agent's scope context, then prove the
423
+ * result is usable before the agent is published.
424
+ *
425
+ * The scope context is what makes the composition per-session: entry contexts
426
+ * chain to the context the subtree was plugged into, so every `ctx.tools`
427
+ * and `ctx.systemPrompt` registration inside the preset files into that
428
+ * agent's layer and unwinds with it. Two guards make that safe. A row that
429
+ * never reached a usable state is rejected, because a directly-plugged subtree
430
+ * is absent from `ctx.loader.entries()` and no boot audit covers it. A row that
431
+ * published a service into the ROOT realm is rejected, because such a service
432
+ * is process-global rather than per-session and the second session mounting the
433
+ * same preset collides with the first.
434
+ * @module @deepseek-ai/dsh-agent-presets/mount
435
+ */
436
+ /**
437
+ * Subtrees captured by config identity. A subtree plugged directly (rather than
438
+ * created as a loader entry) never links itself to an `Entry`, so this is the
439
+ * only handle to the rows it created; config objects are minted per mount, so
440
+ * concurrent mounts cannot collide.
441
+ */
442
+ const mounted = /* @__PURE__ */ new WeakMap();
443
+ /**
444
+ * The base URL bare specifiers resolve against, per pending mount, keyed by the
445
+ * same config object. Recorded before the subtree is plugged, because `Include`
446
+ * rewrites its own context's `baseUrl` to the composition's directory and the
447
+ * pre-mount value is the only handle on where the harness itself lives.
448
+ */
449
+ const harnessBase = /* @__PURE__ */ new WeakMap();
450
+ /**
451
+ * Include subclass that publishes its tree and fiber for the audit, and never
452
+ * writes to the file it read.
453
+ */
454
+ var PresetTree = class extends Include {
455
+ constructor(ctx, config) {
456
+ super(ctx, config);
457
+ mounted.set(config, {
458
+ tree: this,
459
+ fiber: ctx.fiber
460
+ });
461
+ }
462
+ /**
463
+ * Resolve a bare specifier from the harness rather than from the preset.
464
+ *
465
+ * `EntryTree.import()` resolves against the tree's own `baseUrl`, which
466
+ * `Include` sets to the composition's directory. That is right for a
467
+ * relative specifier — a preset's own files travel with it — and wrong for
468
+ * a package name: a locally authored preset lives under the user's home,
469
+ * where Node's upward `node_modules` walk never reaches the harness's own
470
+ * dependencies, so every `@deepseek-ai/dsh-*` row would fail to import. The
471
+ * mount records the host composition's base instead, which is inside the
472
+ * installed harness, and bare names resolve from there. An absolute
473
+ * filesystem path names neither base and becomes a file URL before Node's
474
+ * ESM loader receives it, which is required for drive-letter paths on
475
+ * Windows.
476
+ * @param name - the module specifier from the row.
477
+ * @param getOuterStack - the loader's stack composer for import diagnostics.
478
+ * @returns the imported module, or the `cordis:` builtin.
479
+ */
480
+ import(name, getOuterStack) {
481
+ const specifier = isAbsolute(name) ? pathToFileURL(name).href : name;
482
+ const base = harnessBase.get(this.config);
483
+ /* v8 ignore next -- every PresetTree is constructed by `mountPreset`, which records the base first */
484
+ if (base === void 0) return super.import(specifier, getOuterStack);
485
+ if (name.startsWith(".") || name.startsWith("cordis:")) return super.import(name, getOuterStack);
486
+ const internal = this.ctx.loader.internal;
487
+ /* v8 ignore next -- Node always supplies the internal module loader; the branch keeps a
488
+ hypothetical embedder from losing the row's name in a resolution error. */
489
+ if (internal === void 0) return super.import(specifier, getOuterStack);
490
+ return internal.import(specifier, base, {});
491
+ }
492
+ /**
493
+ * A preset is an input, never a persistence target.
494
+ *
495
+ * The Loader writes a tree back through this method whenever it decides the
496
+ * config changed — a plugin self-disposing is enough, and tearing an agent
497
+ * down disposes its whole subtree. Inherited, that rewrites the preset file
498
+ * with whatever the dying tree held, which in practice means truncating a
499
+ * shipped composition to `[]` the first time a session ends. Persisting a
500
+ * preset is also meaningless: nothing here is user state, and the same file
501
+ * backs every session that names it.
502
+ *
503
+ * Dropping the write drops the `loader/config-update` the inherited method
504
+ * emits with it. Nothing observes one for a preset subtree today, and a
505
+ * future "edit your preset while it runs" flow needs a deliberate
506
+ * persistence path rather than this method's return.
507
+ */
508
+ write() {}
509
+ };
510
+ const mounts = /* @__PURE__ */ new Set();
511
+ /**
512
+ * Drop every record whose subtree is gone.
513
+ *
514
+ * Records are pruned by observation rather than through a disposal hook
515
+ * because a subtree can be torn down by its owning agent, by a failed mount, or
516
+ * by the whole tree unloading, and a cleared `uid` is what all three share.
517
+ *
518
+ * Pruning therefore has to happen on a path this module owns. Reading is one
519
+ * such path, but not a reliable one: the only production reader is the
520
+ * invariant companion's service listener, and `dsh-invariants` is a
521
+ * development composition — a shipped host never loads it. Mounting is the
522
+ * other, and it is the one every session takes, which bounds the set at one
523
+ * generation of dead records rather than one per session ever composed. Each
524
+ * record would otherwise retain its whole disposed subtree: the fiber holds
525
+ * its config, and that config is the key its `EntryTree` is stored under.
526
+ */
527
+ function pruneDisposedMounts() {
528
+ for (const mount of mounts) if (mount.fiber.uid === null) mounts.delete(mount);
529
+ }
530
+ /**
531
+ * Every preset composition still installed, pruning fibers disposed since the
532
+ * last read.
533
+ * @returns the live mounts.
534
+ */
535
+ function livePresetMounts() {
536
+ pruneDisposedMounts();
537
+ return [...mounts];
538
+ }
539
+ /**
540
+ * Whether `fiber` is `root` itself or is mounted anywhere inside its subtree.
541
+ *
542
+ * Membership is object identity. `uid` looks like a cheaper key but is a
543
+ * per-registry counter, so fibers in two different roots collide on it and a
544
+ * subtree in one runtime would be blamed for a service published in another.
545
+ * @param fiber - the fiber to locate.
546
+ * @param root - the subtree root to test membership against.
547
+ * @returns true when `fiber` belongs to `root`'s subtree.
548
+ */
549
+ function withinFiber(fiber, root) {
550
+ let current = fiber;
551
+ while (true) {
552
+ if (current === root) return true;
553
+ const parent = current.parent.fiber;
554
+ if (parent === current) return false;
555
+ current = parent;
556
+ }
557
+ }
558
+ /**
559
+ * Service names the mounted subtree published into the root realm.
560
+ *
561
+ * A provider without an `isolate` realm stores its implementation under the
562
+ * root's symbol for that name, which is exactly the comparison below; a
563
+ * provider inside an `isolate` realm stores under a realm-private symbol and
564
+ * is correctly absent here.
565
+ * @param ctx - any context of the runtime whose service store is inspected.
566
+ * @param mount - the mounted subtree's fiber.
567
+ * @returns the leaked service names in lexical order.
568
+ */
569
+ function leakedServices(ctx, mount) {
570
+ const store = ctx.reflect.store;
571
+ const rootIsolate = ctx.root[Context.isolate];
572
+ const leaked = [];
573
+ for (const key of Object.getOwnPropertySymbols(store)) {
574
+ const impl = store[key];
575
+ /* v8 ignore next -- cordis deletes a store slot on disposal rather than
576
+ clearing it, so an own symbol always resolves; the guard exists only
577
+ because the store's index signature is optional. */
578
+ if (impl === void 0) continue;
579
+ if (!withinFiber(impl.fiber, mount)) continue;
580
+ if (rootIsolate[impl.name] === key) leaked.push(impl.name);
581
+ }
582
+ return leaked.sort((left, right) => left.localeCompare(right));
583
+ }
584
+ /**
585
+ * The standing composition one agent is joined to.
586
+ *
587
+ * The agent's own key is parented to its preset's standing key, so the mount
588
+ * is found by matching that parent rather than by walking up from the agent —
589
+ * the mount is not under the agent's fiber. An agent that joined no preset —
590
+ * a deployment composing no roster, or a child agent before its join — has no
591
+ * parent link and resolves to undefined.
592
+ * @param agentCtx - the agent's scope context.
593
+ * @returns the mount the agent joined, or undefined when it joined none.
594
+ */
595
+ function standingMountFor(agentCtx) {
596
+ const agentKey = scopeOf(agentCtx);
597
+ if (agentKey === void 0) return void 0;
598
+ const standingKey = scopeParentOf(agentKey);
599
+ if (standingKey === void 0) return void 0;
600
+ return livePresetMounts().find((candidate) => candidate.key === standingKey);
601
+ }
602
+ /**
603
+ * One agent's instance of a service its preset mounted.
604
+ *
605
+ * A preset publishes a service behind an `isolate` realm so two sessions
606
+ * cannot collide, and an entry-local realm is invisible to everything outside
607
+ * the group — including the agent's own scope context and the host. That is
608
+ * right for the rows inside the group and wrong for one caller: a request that
609
+ * is ABOUT a session but arrives from outside it, which is every browser RPC
610
+ * the api-proxy serves.
611
+ *
612
+ * Ownership is the same relation {@link leakedServices} reads, inverted: there
613
+ * it names implementations a subtree published into the ROOT realm, here it
614
+ * names the one this subtree published anywhere. Fiber membership is object
615
+ * identity for the reason stated on {@link withinFiber}.
616
+ *
617
+ * This is READ addressing for a caller that already holds the agent. It is not
618
+ * a general host handle on a session's internals: a host row that `inject`s a
619
+ * service cannot use it, because injection resolves before any session exists
620
+ * and has no agent to key by — such a service belongs on the host plane.
621
+ * @param ctx - any context of the runtime whose service store is inspected.
622
+ * @param agent - the agent whose mounted composition to look inside.
623
+ * @param name - the service name as the preset's rows resolve it.
624
+ * @returns the agent's instance, or undefined when its preset mounts none.
625
+ */
626
+ function serviceForAgent(ctx, agent, name) {
627
+ const mount = standingMountFor(agent.ctx);
628
+ if (mount === void 0) return void 0;
629
+ const store = ctx.reflect.store;
630
+ for (const key of Object.getOwnPropertySymbols(store)) {
631
+ const impl = store[key];
632
+ /* v8 ignore next -- cordis deletes a store slot on disposal rather than clearing it */
633
+ if (impl === void 0) continue;
634
+ if (impl.name !== name) continue;
635
+ if (withinFiber(impl.fiber, mount.fiber)) return impl.value;
636
+ }
637
+ }
638
+ /**
639
+ * Rows that did not reach a usable state, each rendered as one diagnostic line.
640
+ *
641
+ * A row whose module failed to import or whose plugin threw already rejects the
642
+ * mount through the loader; what remains observable here is a row still waiting
643
+ * for a service the composition never supplies.
644
+ * @param tree - the mounted subtree.
645
+ * @returns one line per unusable row, empty when every enabled row is usable.
646
+ */
647
+ function inactiveRows(tree) {
648
+ const lines = [];
649
+ for (const entry of tree.entries()) {
650
+ if (entry.disabled) continue;
651
+ const fiber = entry.fiber;
652
+ /* v8 ignore next 4 -- the loader rejects an entry whose module or plugin failed,
653
+ so a settled tree never holds an enabled fiber-less entry; the branch exists
654
+ only because `Entry.fiber` is declared optional. */
655
+ if (fiber === void 0) {
656
+ lines.push(`${entry.options.id} (${entry.options.name}): never started`);
657
+ continue;
658
+ }
659
+ const missing = Object.keys(fiber.inject).filter((name) => fiber.ctx.get(name) === void 0);
660
+ if (missing.length > 0) lines.push(`${entry.options.id} (${entry.options.name}): waiting for ${missing.join(", ")}`);
661
+ }
662
+ return lines;
663
+ }
664
+ /**
665
+ * The reportable text of a mount failure.
666
+ *
667
+ * The loader reports several failed rows as one `AggregateError`, whose own
668
+ * message names none of them; without flattening, a composition that fails on
669
+ * two rows says only "loader entries failed to apply" and the operator has
670
+ * nothing to act on.
671
+ * @param error - the value the mount rejected with.
672
+ * @returns a single-line-per-cause description.
673
+ */
674
+ function mountDetail(error) {
675
+ /* v8 ignore next -- every path into the mount's catch throws an Error: the loader
676
+ wraps a row's thrown value before it propagates, and this module's own
677
+ rejections are Errors. The fallback keeps a hostile value readable. */
678
+ if (!(error instanceof Error)) return String(error);
679
+ if (!(error instanceof AggregateError)) return error.message;
680
+ return [error.message, ...error.errors.map((cause) => `- ${mountDetail(cause)}`)].join("\n");
681
+ }
682
+ /**
683
+ * Mount `preset` under `agentCtx` and return only once every row is usable.
684
+ *
685
+ * The subtree is owned by `agentCtx`'s fiber, so it unwinds with the agent and
686
+ * the caller receives no disposer. A rejection leaves nothing mounted.
687
+ * @param agentCtx - the agent's scope context, from the agent factory's `setup`.
688
+ * @param preset - the resolved preset to compose the agent from.
689
+ * @throws when `agentCtx` carries no scope, a row is unusable, or a row
690
+ * published a service into the root realm.
691
+ */
692
+ async function mountPreset(agentCtx, preset) {
693
+ if (scopeOf(agentCtx) === void 0) throw new Error(`agent-presets: refusing to mount preset "${preset.id}" into an unscoped context; its registrations would apply to every agent in the process`);
694
+ const config = { path: pathToFileURL(preset.path).href };
695
+ /* v8 ignore next -- the Loader sets `baseUrl` on the root before any scoped context derives from it */
696
+ if (agentCtx.baseUrl !== void 0) harnessBase.set(config, agentCtx.baseUrl);
697
+ pruneDisposedMounts();
698
+ const handle = agentCtx.plugin(PresetTree, config);
699
+ try {
700
+ await handle.await();
701
+ const subtree = mounted.get(config);
702
+ /* v8 ignore next -- the subclass constructor runs before `await()` settles for every mounted tree */
703
+ if (subtree === void 0) throw new Error("mounted subtree did not publish its entry tree");
704
+ const { tree, fiber } = subtree;
705
+ const unusable = inactiveRows(tree);
706
+ if (unusable.length > 0) throw new Error(`${String(unusable.length)} row(s) did not activate:\n${unusable.join("\n")}`);
707
+ const leaked = leakedServices(agentCtx, fiber);
708
+ if (leaked.length > 0) throw new Error(`row(s) published process-global service(s) [${leaked.join(", ")}]; a preset service must sit behind an \`isolate\` realm or move to the host composition`);
709
+ mounts.add({
710
+ presetId: preset.id,
711
+ fiber,
712
+ key: scopeOf(agentCtx)
713
+ });
714
+ } catch (error) {
715
+ try {
716
+ await handle.dispose();
717
+ } catch {}
718
+ throw new PresetMountError(preset.id, `${mountDetail(error)} (${preset.path})`, { cause: error });
719
+ }
720
+ }
721
+ //#endregion
722
+ //#region src/index.ts
723
+ /**
724
+ * Agent presets: each session composes its model-facing plugin set from one
725
+ * preset `cordis.yml`, mounted ONCE per preset under a standing scope and
726
+ * joined by every agent that names it.
727
+ *
728
+ * The standing mount is what makes a preset one composition rather than one
729
+ * per session: its plugin instances, tool registrations, prompt sections, and
730
+ * projection units exist exactly once, keyed per session inside the plugins
731
+ * themselves (they predate presets and were written for a shared world). An
732
+ * agent joins by having its scope key parented to the mount's
733
+ * ({@link bindScopeParent}), which makes the mount's registrations visible to
734
+ * that agent's views and the mount's listeners receive that agent's events —
735
+ * and a host reader with no agent at all (a cold transcript read) resolves
736
+ * the same standing registrations by preset id.
737
+ *
738
+ * This package owns the preset vocabulary, filesystem discovery, and the
739
+ * guarded standing mount. It does not decide when an agent is created — the
740
+ * agent factory's `setup(agentCtx)` hook is the one supported call site,
741
+ * because only there is the join installed while the agent is still
742
+ * unpublished, so a rejected composition rolls the whole creation back.
743
+ * @module @deepseek-ai/dsh-agent-presets
744
+ */
745
+ /** Settings namespace carrying the user's chosen default preset. */
746
+ const SETTINGS_NAMESPACE = "agent-presets";
747
+ /** Runtime schema for the user-writable slice. */
748
+ const AgentPresetSettingsSchema = z.object({ default: z.string() });
749
+ (class extends Service {
750
+ config;
751
+ static inject = ["loader"];
752
+ /** Runtime schema for the preset roster. */
753
+ static Config = z.object({
754
+ default: z.string().required(),
755
+ roots: z.array(z.object({
756
+ path: z.string().required(),
757
+ trust: z.union(["system", "user"]).default("user")
758
+ })).default([])
759
+ });
760
+ /**
761
+ * The user layer over `config.default`, present only while a settings
762
+ * provider is composed. Held rather than snapshotted so a hot-reloaded
763
+ * document takes effect without a restart.
764
+ */
765
+ settings;
766
+ /**
767
+ * The settings service behind {@link settings}, held for the one write this
768
+ * service makes: clearing a user default it has just deleted.
769
+ */
770
+ settingsService;
771
+ /**
772
+ * The service's own untraced context. Methods invoked through the traceable
773
+ * proxy see `this.ctx` rebound to the CALLER's context, which carries a
774
+ * shadow; a subtree minted from it resolves every service through that
775
+ * shadow's fiber instead of each entry's own inject store, so preset rows
776
+ * would fail on the very services they declare. Standing mounts must hang
777
+ * off the untraced original (the `tasks-local` selfCtx precedent).
778
+ */
779
+ selfCtx;
780
+ constructor(ctx, config) {
781
+ super(ctx, "agentPresets");
782
+ this.config = config;
783
+ this.selfCtx = ctx;
784
+ ctx.inject(["settings"], (settingsCtx) => {
785
+ this.settings = settingsCtx.settings.register(settingsNamespace(SETTINGS_NAMESPACE), AgentPresetSettingsSchema, { base: { default: config.default } });
786
+ this.settingsService = settingsCtx.settings;
787
+ settingsCtx.effect(() => () => {
788
+ this.settings = void 0;
789
+ this.settingsService = void 0;
790
+ }, "agentPresets.settings()");
791
+ });
792
+ }
793
+ /**
794
+ * The preset id mounted when a caller names none.
795
+ *
796
+ * Read per call rather than cached: the settings document is hot-reloaded, so
797
+ * changing the default takes effect on the next session created and leaves
798
+ * every running session on the preset it was composed from.
799
+ */
800
+ get defaultId() {
801
+ return this.settings?.get().default ?? this.config.default;
802
+ }
803
+ /**
804
+ * Every preset the configured roots currently supply.
805
+ * @returns the presets, first-root-wins per id.
806
+ */
807
+ async list() {
808
+ return await discoverPresets(this.config.roots);
809
+ }
810
+ /**
811
+ * Resolve one preset by id.
812
+ *
813
+ * A broken preset resolves — deleting one, reading one, and reporting one
814
+ * all need the row — and the mounting paths refuse it AFTER resolution
815
+ * through {@link resolveMountable}.
816
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
817
+ * @returns the resolved preset.
818
+ * @throws when no configured root supplies that id.
819
+ */
820
+ async resolve(id) {
821
+ const wanted = id ?? this.defaultId;
822
+ const presets = await this.list();
823
+ const found = presets.find((preset) => preset.id === wanted);
824
+ if (found === void 0) throw new UnknownPresetError(wanted, presets.map((preset) => preset.id));
825
+ return found;
826
+ }
827
+ /**
828
+ * Resolve one preset that is about to compose an agent, refusing a broken
829
+ * one with its discovery-reported reason. Failing here rather than inside
830
+ * the loader keeps the answer the same for every unloadable shape — ghost
831
+ * directory, unparsable YAML, rowless list — and spends no mount attempt
832
+ * on a composition discovery already read as unusable.
833
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
834
+ * @returns the resolved, mountable preset.
835
+ * @throws when the preset is unknown or discovery reports it broken.
836
+ */
837
+ async resolveMountable(id) {
838
+ const preset = await this.resolve(id);
839
+ if (preset.broken !== void 0) throw new PresetMountError(preset.id, preset.broken);
840
+ return preset;
841
+ }
842
+ /**
843
+ * Standing mounts by preset id, single-flight so two agents racing the
844
+ * first use of one preset share one composition. A settled failure is
845
+ * removed so a later session retries a preset whose file has been fixed; a
846
+ * settled success serves until the composition FILE visibly changes — each
847
+ * generation records its file stamp, and a stale stamp starts the next
848
+ * generation for sessions created afterwards. Sessions already joined keep
849
+ * the generation they run on; a superseded one is never disposed while the
850
+ * process lives (reclaimed only by whole-tree teardown), so editing files
851
+ * is bounded by how often compositions change, not by session count.
852
+ */
853
+ standing = /* @__PURE__ */ new Map();
854
+ /**
855
+ * Parent bindings of the agents this roster composed, keyed by the agent's
856
+ * scope key. The binding is dsh-scope's only re-link capability; holding it
857
+ * here makes this service the sole authority that can move an agent between
858
+ * standing compositions. WeakMap: entries die with their agents.
859
+ */
860
+ bindings = /* @__PURE__ */ new WeakMap();
861
+ /**
862
+ * Compose one agent from a preset: ensure the preset's standing mount, then
863
+ * parent the agent's scope key to it so the mount's registrations and
864
+ * listeners cover this agent.
865
+ *
866
+ * Call from the agent factory's `setup(agentCtx)`; a rejection there rolls
867
+ * the agent creation back, so a broken preset never yields a half-composed
868
+ * session.
869
+ * @param agentCtx - the agent's scope context.
870
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
871
+ * @returns the preset that was composed, for the caller to record.
872
+ * @throws when the preset is unknown or its composition is unusable.
873
+ */
874
+ async mount(agentCtx, id) {
875
+ const agentKey = scopeOf(agentCtx);
876
+ if (agentKey === void 0) throw new Error("agent-presets: refusing to compose an unscoped context; the scope key is what joins an agent to its preset");
877
+ const preset = await this.resolveMountable(id);
878
+ const standing = await this.ensureStanding(preset);
879
+ this.bindings.set(agentKey, bindScopeParent(agentKey, standing.key));
880
+ return preset;
881
+ }
882
+ /**
883
+ * Join one agent to the SAME standing composition another already runs on.
884
+ *
885
+ * This is how a child agent inherits its parent's capabilities. It is a bind,
886
+ * not a mount: the parent's generation is already composed, so the child gets
887
+ * that exact instance — the same plugin objects, the same tool registrations,
888
+ * the same prompt sections. Re-resolving the parent's preset by id instead
889
+ * would re-read the roster, and a composition file edited since the parent
890
+ * started would hand the child a DIFFERENT generation than the one its
891
+ * parent's history was produced under (and a preset deleted since would fail
892
+ * the child outright while its parent keeps running).
893
+ *
894
+ * Synchronous, and with no composition failure mode of its own — it reads no
895
+ * roster, mounts nothing, and touches no file — which is what lets a child
896
+ * creation window use it: the two in-process subagent drivers compose their
897
+ * children inside a synchronous `setup`. It still rejects a caller error, as
898
+ * the `@throws` below record.
899
+ *
900
+ * A parent that joined no preset — a rosterless deployment — yields no join
901
+ * and no error: there, the model-facing rows sit in the host composition and
902
+ * the child already sees them through the global layer.
903
+ * @param agentCtx - the joining agent's scope context.
904
+ * @param parentCtx - the scope context of the agent whose composition to join.
905
+ * @returns the preset id joined, or undefined when the parent joined none.
906
+ * @throws when `agentCtx` carries no scope, or has already joined a preset.
907
+ */
908
+ composeFrom(agentCtx, parentCtx) {
909
+ const agentKey = scopeOf(agentCtx);
910
+ if (agentKey === void 0) throw new Error("agent-presets: refusing to compose an unscoped context; the scope key is what joins an agent to its preset");
911
+ const standing = standingMountFor(parentCtx);
912
+ if (standing === void 0) return void 0;
913
+ this.bindings.set(agentKey, bindScopeParent(agentKey, standing.key));
914
+ return standing.presetId;
915
+ }
916
+ /**
917
+ * The preset one live agent runs on.
918
+ *
919
+ * Read from the live scope chain rather than from the session, so it answers
920
+ * for an agent whose session has not recorded a preset yet — a child agent
921
+ * whose durable header is being built from its parent's composition.
922
+ * @param agentCtx - the agent's scope context.
923
+ * @returns the preset id, or undefined when the agent joined none.
924
+ */
925
+ composedPreset(agentCtx) {
926
+ return standingMountFor(agentCtx)?.presetId;
927
+ }
928
+ /** Whether this deployment configures a root locally authored presets go to. */
929
+ get authorable() {
930
+ return this.config.roots.some((root) => root.trust === "user");
931
+ }
932
+ /**
933
+ * Read one preset's composition text.
934
+ * @param id - the preset id.
935
+ * @returns the composition exactly as stored.
936
+ * @throws when no configured root supplies that id.
937
+ */
938
+ async read(id) {
939
+ return await readComposition(await this.resolve(id));
940
+ }
941
+ /**
942
+ * Create a locally authored preset by copying an existing one whole.
943
+ *
944
+ * Copy is the only authoring write. Composition text never crosses this
945
+ * seam: the source is named by id and its directory is copied as it stands,
946
+ * so the copy is exactly as loadable as its source and authoring grants no
947
+ * capability the roster did not already carry. The copy is NOT mounted to
948
+ * validate — a source that mounts today yields a copy that mounts today.
949
+ * @param from - the preset the copy starts from; shipped presets are the
950
+ * primary source, so any trust is accepted.
951
+ * @param id - the new preset's id, which becomes its directory name.
952
+ * @param name - display name for the copy; absent falls back to the id.
953
+ * @throws when the source is unknown, the id is unusable or already taken,
954
+ * or the deployment configures no writable root.
955
+ */
956
+ async copy(from, id, name) {
957
+ const source = await this.resolve(from);
958
+ if ((await this.list()).some((preset) => preset.id === id)) throw new PresetExistsError(id);
959
+ await copyComposition(this.config.roots, source, id, name);
960
+ this.standing.delete(id);
961
+ }
962
+ /**
963
+ * Delete a locally authored preset.
964
+ * @param id - the preset id.
965
+ * @throws when the preset is unknown or ships with the deployment.
966
+ */
967
+ async remove(id) {
968
+ await deleteComposition(this.config.roots, await this.resolve(id));
969
+ this.standing.delete(id);
970
+ if (this.settings?.get().default !== id) return;
971
+ await this.settingsService?.mutate(settingsNamespace(SETTINGS_NAMESPACE), [{
972
+ op: "unset",
973
+ path: ["default"]
974
+ }]);
975
+ }
976
+ /**
977
+ * One agent's instance of a service its preset mounted.
978
+ *
979
+ * A preset publishes services behind `isolate` realms, which are invisible
980
+ * outside the group that declares them — including to the host. This is how a
981
+ * caller holding the agent reads one anyway: a request that is ABOUT a
982
+ * session but arrives from outside it, which is every browser RPC.
983
+ *
984
+ * Read addressing only. A host row that `inject`s a service cannot use this,
985
+ * because injection resolves before any session exists and has no agent to
986
+ * key by; such a service belongs on the host plane instead.
987
+ * @param agent - the agent whose composition to look inside.
988
+ * @param name - the service name as the preset's rows resolve it.
989
+ * @returns the agent's instance, or undefined when its preset mounts none.
990
+ */
991
+ serviceFor(agent, name) {
992
+ return serviceForAgent(this.ctx, agent, name);
993
+ }
994
+ /**
995
+ * Re-link one agent to a different preset's standing composition.
996
+ *
997
+ * Only valid while the agent has produced nothing: swapping tools mid
998
+ * conversation would leave logged tool calls the new composition cannot
999
+ * make. The CALLER owns that check — this method does not read session
1000
+ * history.
1001
+ *
1002
+ * The swap is a parent re-link, not an unmount: standing mounts are shared
1003
+ * and permanent, so the old composition stays for its other agents and the
1004
+ * new one is ensured BEFORE the link moves. An unknown or unusable preset
1005
+ * therefore throws with the agent exactly as it was — there is no torn-down
1006
+ * state to restore. The re-link runs through the binding this roster kept
1007
+ * from the agent's mount — dsh-scope's only re-link authority. An agent
1008
+ * that never composed one has nothing to re-link: the switch is then the
1009
+ * agent's first bind, exactly a mount.
1010
+ * @param agentCtx - the agent's scope context.
1011
+ * @param id - the preset to compose the agent from instead.
1012
+ * @returns the preset now installed.
1013
+ * @throws when the preset is unknown or its composition is unusable.
1014
+ */
1015
+ async recompose(agentCtx, id) {
1016
+ const agentKey = scopeOf(agentCtx);
1017
+ if (agentKey === void 0) throw new Error("agent-presets: refusing to recompose an unscoped context");
1018
+ const preset = await this.resolveMountable(id);
1019
+ const standing = await this.ensureStanding(preset);
1020
+ const binding = this.bindings.get(agentKey);
1021
+ if (binding === void 0) this.bindings.set(agentKey, bindScopeParent(agentKey, standing.key));
1022
+ else binding.rebind(standing.key);
1023
+ return preset;
1024
+ }
1025
+ /**
1026
+ * The standing scope key of one preset, for a host reader with no agent.
1027
+ *
1028
+ * A cold transcript read resolves tool presenters against the composition
1029
+ * the session recorded, and the standing mount makes that possible without
1030
+ * resuming anything: ensuring the mount composes plugins but starts no
1031
+ * agent, no session, and no turn.
1032
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
1033
+ * @returns the standing scope key readers pass as a registry view scope.
1034
+ * @throws when the preset is unknown or its composition is unusable.
1035
+ */
1036
+ async standingKeyFor(id) {
1037
+ const preset = await this.resolveMountable(id);
1038
+ return (await this.ensureStanding(preset)).key;
1039
+ }
1040
+ /** Resolve (or create, single-flight) the standing mount of one preset. */
1041
+ async ensureStanding(preset) {
1042
+ const pending = this.standing.get(preset.id);
1043
+ if (pending !== void 0) {
1044
+ const mounted = await pending;
1045
+ const current = await compositionStamp(preset.path);
1046
+ if (current === void 0 || sameStamp(mounted.stamp, current)) return mounted;
1047
+ if (this.standing.get(preset.id) === pending) this.standing.delete(preset.id);
1048
+ return this.ensureStanding(preset);
1049
+ }
1050
+ const created = (async () => {
1051
+ const key = { agentPreset: preset.id };
1052
+ const scope = createScope(this.selfCtx, key);
1053
+ try {
1054
+ const stamp = await compositionStamp(preset.path);
1055
+ if (stamp === void 0) throw new PresetMountError(preset.id, `composition file is unreadable: ${preset.path}`);
1056
+ await mountPreset(scope.ctx, preset);
1057
+ return {
1058
+ key,
1059
+ scope,
1060
+ stamp
1061
+ };
1062
+ } catch (error) {
1063
+ this.standing.delete(preset.id);
1064
+ await scope.dispose();
1065
+ throw error;
1066
+ }
1067
+ })();
1068
+ this.standing.set(preset.id, created);
1069
+ return created;
1070
+ }
1071
+ });
1072
+ /** Read one composition file's stamp, or undefined when it cannot be statted. */
1073
+ async function compositionStamp(path) {
1074
+ try {
1075
+ const { mtimeMs, size } = await stat(path);
1076
+ return {
1077
+ mtimeMs,
1078
+ size
1079
+ };
1080
+ } catch {
1081
+ return;
1082
+ }
1083
+ }
1084
+ /** Whether two stamps name the same file state. */
1085
+ function sameStamp(a, b) {
1086
+ return a.mtimeMs === b.mtimeMs && a.size === b.size;
1087
+ }
1088
+ //#endregion
1089
+ //#region lib/types/invariant.js
1090
+ /**
1091
+ * Package-owned invariant companion for `@deepseek-ai/dsh-agent-presets`.
1092
+ * @module @deepseek-ai/dsh-agent-presets/invariant
1093
+ */
1094
+ const PACKAGE_NAME = "@deepseek-ai/dsh-agent-presets";
1095
+ /** Cordis companion plugin name. */
1096
+ const name = "agent-presets-invariant";
1097
+ /** Service required before the companion can reserve package ownership. */
1098
+ const inject = ["invariants"];
1099
+ /**
1100
+ * Assert that no installed preset composition reaches the root service realm.
1101
+ *
1102
+ * `mountPreset` proves this once, when the subtree settles. A row that
1103
+ * publishes later — from a timer, or an asynchronous continuation after its
1104
+ * plugin returned — would escape that one-shot audit, so re-check every live
1105
+ * mount whenever a service registration changes.
1106
+ */
1107
+ const install = (ctx, fail) => {
1108
+ ctx.on("internal/service", function(name) {
1109
+ for (const mount of livePresetMounts()) {
1110
+ const leaked = leakedServices(ctx, mount.fiber);
1111
+ if (leaked.length === 0) continue;
1112
+ fail(`preset "${mount.presetId}" published process-global service(s) [${leaked.join(", ")}] after its mount was audited (observed while notifying "${name}") — a preset service must sit behind an \`isolate\` realm or move to the host composition`);
1113
+ }
1114
+ }, { global: true });
1115
+ };
1116
+ /**
1117
+ * Register this package's invariant companion.
1118
+ * @param ctx - Cordis context carrying the invariant service.
1119
+ * @returns the installed registration's disposer after setup succeeds.
1120
+ */
1121
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
1122
+ //#endregion
1123
+ export { apply, inject, name };