@khorsheed/dsh-local-agent-dsh 0.1.0-rc.6

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.
Files changed (45) hide show
  1. package/README.en.md +150 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +151 -0
  4. package/cordis.patch.yml +24 -0
  5. package/lib/client.js +1115 -0
  6. package/lib/dsh-cli-provider-UhWvivte.js +1701 -0
  7. package/lib/dsh-cli-provider.js +3 -0
  8. package/lib/index.js +624 -0
  9. package/lib/invariant.js +33 -0
  10. package/lib/provision.js +602 -0
  11. package/lib/records.js +109 -0
  12. package/lib/session-log-DOsSz56-.js +88 -0
  13. package/lib/types/client/SettingsCard.d.ts +78 -0
  14. package/lib/types/client/SettingsCard.js +200 -0
  15. package/lib/types/client/icons.d.ts +18 -0
  16. package/lib/types/client/icons.js +2 -0
  17. package/lib/types/client/index.d.ts +45 -0
  18. package/lib/types/client/index.js +88 -0
  19. package/lib/types/client/locales.d.ts +76 -0
  20. package/lib/types/client/locales.js +67 -0
  21. package/lib/types/dsh-cli-provider.d.ts +213 -0
  22. package/lib/types/dsh-cli-provider.js +733 -0
  23. package/lib/types/index.d.ts +124 -0
  24. package/lib/types/index.js +347 -0
  25. package/lib/types/invariant.d.ts +16 -0
  26. package/lib/types/invariant.js +37 -0
  27. package/lib/types/live-driver.d.ts +180 -0
  28. package/lib/types/live-driver.js +887 -0
  29. package/lib/types/live-switch.d.ts +76 -0
  30. package/lib/types/live-switch.js +100 -0
  31. package/lib/types/model-broker.d.ts +86 -0
  32. package/lib/types/model-broker.js +149 -0
  33. package/lib/types/model-catalog.d.ts +27 -0
  34. package/lib/types/model-catalog.js +71 -0
  35. package/lib/types/provision.d.ts +382 -0
  36. package/lib/types/provision.js +656 -0
  37. package/lib/types/records.d.ts +41 -0
  38. package/lib/types/records.js +125 -0
  39. package/lib/types/session-log.d.ts +58 -0
  40. package/lib/types/session-log.js +83 -0
  41. package/lib/types/session-mirror.d.ts +155 -0
  42. package/lib/types/session-mirror.js +451 -0
  43. package/locale/en.json +6 -0
  44. package/locale/zh.json +6 -0
  45. package/package.json +121 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * dsh harness instantiation of the local-agent family, plus the DeepSeek
3
+ * settings toggle that gates it. Registers the `local-agent-dsh` settings
4
+ * namespace (default off); while ON, the controller registers the `dsh`
5
+ * harness into the core registry (scoped home under the shared homes root,
6
+ * `$DSH_HOME`), the `dsh-cli` delegation provider (sub-dsh headless runs with
7
+ * the parent's resolved DeepSeek key), and the family delegation tool for it.
8
+ * While OFF — the default — nothing model-visible mounts, so the instance
9
+ * keeps exactly the current behavior (official in-process subagent tools) and
10
+ * the model never sees two overlapping delegation tools at once.
11
+ *
12
+ * The same namespace carries the resident-mode preferences (`live`,
13
+ * `liveMirrorGranularity`): the YAML config is the composition base, the
14
+ * settings card's writes are user-layer overrides, and a LiveDriverSwitch
15
+ * inside each enabled generation hot-swaps driver generations on a change —
16
+ * no reload, in-flight rounds never interrupted.
17
+ *
18
+ * The sub-dsh half lives in the sibling bundle
19
+ * `@khorsheed/dsh-local-agent-dsh-headless`: a headless profile under the
20
+ * scoped home that the provider spawns with `--session-id <uuid>` (fresh) or
21
+ * `--resume <uuid>` (continuation).
22
+ *
23
+ * @module @khorsheed/dsh-local-agent-dsh
24
+ */
25
+ import type { Context } from '@deepseek-ai/cordis';
26
+ import z from '@deepseek-ai/schemastery';
27
+ import type { DshSubProfilePermissions } from './provision.ts';
28
+ /** Stable Cordis plugin name; the bundle patch row id. */
29
+ export declare const name = "local-agent-dsh";
30
+ /** Services required before the toggle controller can mount. */
31
+ export declare const inject: string[];
32
+ /** Plugin config: the sub-dsh profile and the credential it resolves. */
33
+ export interface LocalAgentDshConfig {
34
+ /** Sub-dsh profile name under the scoped home. */
35
+ profileName?: string;
36
+ /** Credential reference resolving the sub-dsh API key. */
37
+ apiKeyRef?: string;
38
+ /**
39
+ * dsh launch argv prefix override (node + entry + launcher flags). Absent
40
+ * means replicate the parent instance's own launch.
41
+ */
42
+ cliLaunch?: string[];
43
+ /** Override the headless bundle directory the sub-profile symlinks to. */
44
+ headlessBundleDir?: string;
45
+ /**
46
+ * The sub-dsh's permission boundary: the sandbox mode every confined call
47
+ * runs under, paired with the approval policy `dsh-base`'s own preset table
48
+ * pairs it with. Provisioning writes it into the scope's sub-profile as a
49
+ * patch layer, so a scoped home carries its own boundary — including into a
50
+ * container unit that bind-mounts it.
51
+ *
52
+ * Absent — the default — writes no layer at all: the sub-dsh runs whatever
53
+ * `dsh-base` composes (`workspace-write` plus `ask`), byte for byte the
54
+ * behavior before this key existed. Pin `danger-full-access` only where
55
+ * something else IS the boundary (an evaluation unit, a disposable
56
+ * container); on a developer's machine the sub-dsh shares the real home.
57
+ */
58
+ permissions?: DshSubProfilePermissions;
59
+ /**
60
+ * Live driver: keep one resident sub-dsh serve process per member and drive
61
+ * turns over the family wire (runtime-level interrupt, push-mode mirror)
62
+ * instead of one process per round. Deployment default only — the settings
63
+ * namespace carries it as the composition base, so the settings card can
64
+ * override it live; the exec one-shot stays the fallback whenever the serve
65
+ * channel cannot come up.
66
+ */
67
+ live?: boolean;
68
+ /** Idle lifetime of an unused resident runtime before reclaim. */
69
+ liveIdleMs?: number;
70
+ /** @deprecated Accepted for old profiles; live output is always incremental. */
71
+ liveMirrorGranularity?: 'event' | 'token';
72
+ /**
73
+ * The model every delegation round starts the sub-dsh with, spelled
74
+ * `provider/model` (a bare id names the model and keeps the instance's
75
+ * provider) — passed as the headless launch's `--model`. Absent — the
76
+ * default — passes no flag at all: the sub-dsh runs the host instance's own
77
+ * `agentDefaultModel` selection, exactly as it did before this key existed.
78
+ *
79
+ * A delegation that names its own model outranks this key; the settings
80
+ * card writes the same key, so a change applies to the next round without a
81
+ * reload.
82
+ */
83
+ model?: string;
84
+ /**
85
+ * The DeepSeek delegation toggle (settings field since the settings rewrite:
86
+ * the profile row can preset it — a semantic superset of the old
87
+ * settings-only key). OFF — the default — mounts nothing model-visible.
88
+ */
89
+ enabled?: boolean;
90
+ /** Model identifiers the settings card saved before (settings field). */
91
+ recentModels?: readonly string[];
92
+ }
93
+ export declare const Config: z;
94
+ /**
95
+ * Settings namespace owning the DeepSeek toggle and the live preferences. On
96
+ * 0.1.5 the Cordis config feeds the namespace's composition `base` layer, so
97
+ * a field absent from the user layer inherits the YAML value — the settings
98
+ * card only ever stores deliberate overrides. On rc.1 the namespace is the
99
+ * plugin row id itself: the row config IS the document, and the settings
100
+ * form's user layer rides the profile patch.
101
+ */
102
+ export declare const DSH_SETTINGS_NAMESPACE = "local-agent-dsh";
103
+ /** The resolved settings the controller and the live switch consume. */
104
+ export interface DshSettings {
105
+ /** The DeepSeek delegation toggle. */
106
+ enabled: boolean;
107
+ /** Resident (live) mode. */
108
+ live: boolean;
109
+ /** @deprecated Legacy granularity; live output is always incremental. */
110
+ liveMirrorGranularity: 'event' | 'token';
111
+ /** The delegation-start model, undefined when unset (pass no `--model`). */
112
+ model: string | undefined;
113
+ /** Model identifiers saved before; the input's suggestions. */
114
+ recentModels: readonly string[];
115
+ }
116
+ /**
117
+ * Mount the toggle controller: register the settings namespace, then mirror
118
+ * its value into harness/provider/tool registrations, live.
119
+ * @param ctx - plugin context carrying the family registry, subagents,
120
+ * subprocess, credentials, and settings services.
121
+ * @param config - validated plugin config.
122
+ */
123
+ export declare function apply(ctx: Context, config: LocalAgentDshConfig): void;
124
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,347 @@
1
+ /**
2
+ * dsh harness instantiation of the local-agent family, plus the DeepSeek
3
+ * settings toggle that gates it. Registers the `local-agent-dsh` settings
4
+ * namespace (default off); while ON, the controller registers the `dsh`
5
+ * harness into the core registry (scoped home under the shared homes root,
6
+ * `$DSH_HOME`), the `dsh-cli` delegation provider (sub-dsh headless runs with
7
+ * the parent's resolved DeepSeek key), and the family delegation tool for it.
8
+ * While OFF — the default — nothing model-visible mounts, so the instance
9
+ * keeps exactly the current behavior (official in-process subagent tools) and
10
+ * the model never sees two overlapping delegation tools at once.
11
+ *
12
+ * The same namespace carries the resident-mode preferences (`live`,
13
+ * `liveMirrorGranularity`): the YAML config is the composition base, the
14
+ * settings card's writes are user-layer overrides, and a LiveDriverSwitch
15
+ * inside each enabled generation hot-swaps driver generations on a change —
16
+ * no reload, in-flight rounds never interrupted.
17
+ *
18
+ * The sub-dsh half lives in the sibling bundle
19
+ * `@khorsheed/dsh-local-agent-dsh-headless`: a headless profile under the
20
+ * scoped home that the provider spawns with `--session-id <uuid>` (fresh) or
21
+ * `--resume <uuid>` (continuation).
22
+ *
23
+ * @module @khorsheed/dsh-local-agent-dsh
24
+ */
25
+ import { join } from 'node:path';
26
+ import z from '@deepseek-ai/schemastery';
27
+ import { credentialRef } from '@deepseek-ai/dsh-credentials';
28
+ import { settingsFace, vol } from '@khorsheed/dsh-local-agent';
29
+ import * as toolModule from '@khorsheed/dsh-local-agent-tool-subagent';
30
+ import { CONTAINER_NODE_OPTIONS, DshCliProvider, dshCliVersion } from './dsh-cli-provider.js';
31
+ import { DEFAULT_LIVE_IDLE_MS } from './live-driver.js';
32
+ import { LiveDriverSwitch } from './live-switch.js';
33
+ import { DshModelCatalog } from './model-catalog.js';
34
+ import { DshModelBroker } from './model-broker.js';
35
+ import { listDshSessions } from './records.js';
36
+ import { defaultPresetRoot, DEFAULT_SUB_PROFILE_NAME, provisionDshScope, USER_PRESET_DIR } from './provision.js';
37
+ /** Stable Cordis plugin name; the bundle patch row id. */
38
+ export const name = 'local-agent-dsh';
39
+ /** Services required before the toggle controller can mount. */
40
+ export const inject = ['localAgent', 'subagents', 'subprocess', 'credentials', 'settings'];
41
+ /**
42
+ * Runtime schema so the Loader always passes an object, never undefined.
43
+ * The settings fields (`enabled`, `live`, `liveMirrorGranularity`, `model`,
44
+ * `recentModels`) ride the same schema, marked `.volatile()` where the host's
45
+ * schemastery has it (3.18.4+, the rc.1 line — probed, never sniffed): there
46
+ * the SettingsForms writes hot-track the running fiber's Volatile references
47
+ * without a remount, and the official one-shot import moves an old
48
+ * settings.yaml section into this row's config. On 0.1.5 the call is absent,
49
+ * the fields stay plain config keys (a semantic superset — the profile row
50
+ * can now preset them too), and the legacy settings namespace below carries
51
+ * the user layer exactly as before.
52
+ *
53
+ * Bare `z` annotation: `z<LocalAgentDshConfig>` fails schemastery 3.18.4's
54
+ * variance under exactOptionalPropertyTypes (TS2375) and dropping the
55
+ * annotation trips TS2742; the interface stays the apply signature's contract.
56
+ */
57
+ const SETTINGS_FIELDS = {
58
+ enabled: z.boolean().default(false),
59
+ live: z.boolean().default(false),
60
+ liveMirrorGranularity: z.union([z.const('event'), z.const('token')]).default('token'),
61
+ // `model` deliberately carries NO default: an unset key must resolve to
62
+ // undefined, which is what keeps the pre-key behavior byte-identical.
63
+ model: z.string(),
64
+ recentModels: z.array(z.string()).default([]),
65
+ };
66
+ /** Mark one schema field volatile when the host's schemastery has the method (rc.1), pass it through plain when not (0.1.5). */
67
+ function volatilize(field) {
68
+ return typeof field.volatile === 'function' ? field.volatile() : field;
69
+ }
70
+ export const Config = z.object({
71
+ profileName: z.string().default(DEFAULT_SUB_PROFILE_NAME),
72
+ apiKeyRef: z.string().default('DEEPSEEK_API_KEY'),
73
+ cliLaunch: z.array(z.string()),
74
+ headlessBundleDir: z.string(),
75
+ permissions: z.union([z.const('read-only'), z.const('workspace-write'), z.const('danger-full-access')]),
76
+ model: volatilize(SETTINGS_FIELDS.model),
77
+ live: volatilize(SETTINGS_FIELDS.live),
78
+ liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
79
+ liveMirrorGranularity: volatilize(SETTINGS_FIELDS.liveMirrorGranularity),
80
+ enabled: volatilize(SETTINGS_FIELDS.enabled),
81
+ recentModels: volatilize(SETTINGS_FIELDS.recentModels),
82
+ });
83
+ /**
84
+ * Settings namespace owning the DeepSeek toggle and the live preferences. On
85
+ * 0.1.5 the Cordis config feeds the namespace's composition `base` layer, so
86
+ * a field absent from the user layer inherits the YAML value — the settings
87
+ * card only ever stores deliberate overrides. On rc.1 the namespace is the
88
+ * plugin row id itself: the row config IS the document, and the settings
89
+ * form's user layer rides the profile patch.
90
+ */
91
+ export const DSH_SETTINGS_NAMESPACE = 'local-agent-dsh';
92
+ /**
93
+ * The card's 0.1.5 schema: `enabled` OFF (default) means the official
94
+ * in-process subagent stays the only delegation path; the live fields mirror
95
+ * the YAML config's deployment defaults through the composition base.
96
+ */
97
+ const DSH_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
98
+ /** The delegation tool the toggle mounts while ON. */
99
+ const DSH_TOOL_NAME = 'subagent_dsh';
100
+ /** Resolve the sub-dsh credential or report unconfigured. */
101
+ async function resolveApiKey(ctx, config) {
102
+ return ctx.credentials.resolve(credentialRef(config.apiKeyRef ?? 'DEEPSEEK_API_KEY'));
103
+ }
104
+ /**
105
+ * Mount the toggle controller: register the settings namespace, then mirror
106
+ * its value into harness/provider/tool registrations, live.
107
+ * @param ctx - plugin context carrying the family registry, subagents,
108
+ * subprocess, credentials, and settings services.
109
+ * @param config - validated plugin config.
110
+ */
111
+ export function apply(ctx, config) {
112
+ ctx.effect(() => {
113
+ // The settings face hides the two host lines: 0.1.5 serves the legacy
114
+ // namespace scope (register + base); rc.1 reads the row config's volatile
115
+ // fields and re-reads on `settings/document-updated`. Both hot-apply the
116
+ // card's writes without a reload.
117
+ const readSettings = () => ({
118
+ enabled: vol(config.enabled) ?? false,
119
+ live: vol(config.live) ?? false,
120
+ liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? 'token',
121
+ model: vol(config.model),
122
+ recentModels: vol(config.recentModels) ?? [],
123
+ });
124
+ const legacyBase = {};
125
+ for (const field of ['enabled', 'live', 'liveMirrorGranularity', 'model']) {
126
+ const value = vol(config[field]);
127
+ if (value !== undefined)
128
+ legacyBase[field] = value;
129
+ }
130
+ const face = settingsFace(ctx, {
131
+ namespace: DSH_SETTINGS_NAMESPACE,
132
+ legacySchema: DSH_SETTINGS_SCHEMA,
133
+ legacyBase,
134
+ read: readSettings,
135
+ });
136
+ // Read PER ROUND, not captured at apply: the settings card writes the same
137
+ // field, so a change has to reach the next delegation without a
138
+ // plugin reload. Blank is not a model — a whitespace-only value is unset.
139
+ const resolveModel = () => {
140
+ const model = face.get().model?.trim();
141
+ return model === undefined || model === '' ? undefined : model;
142
+ };
143
+ // The host instance's default model selection spelled `provider/model` —
144
+ // what a sub-dsh with no `--model` inherits. Undefined when the service or
145
+ // the selection is unreadable: absence is the honest answer, never a guess.
146
+ const hostDefaultModel = () => {
147
+ try {
148
+ const defaultModel = ctx.get('agentDefaultModel');
149
+ const selection = defaultModel?.currentSelection?.();
150
+ if (typeof selection?.model === 'string' && selection.model !== '') {
151
+ return typeof selection.provider === 'string' && selection.provider !== ''
152
+ ? `${selection.provider}/${selection.model}`
153
+ : selection.model;
154
+ }
155
+ }
156
+ catch {
157
+ // Degrade: an unreadable selection reports no model instead of
158
+ // breaking the status surface.
159
+ }
160
+ return undefined;
161
+ };
162
+ const modelCatalog = new DshModelCatalog({
163
+ llm: () => ctx.get('llm'),
164
+ defaultModel: hostDefaultModel,
165
+ });
166
+ void modelCatalog.read();
167
+ ctx.on('llm/adapters-updated', () => { modelCatalog.invalidate(); });
168
+ // The member-level model surface: the composer picker's session-level
169
+ // overrides (in-memory, deliberately lost on a host restart), shared by
170
+ // reference with the broker (which writes them), the exec provider (which
171
+ // consults them per round), and the live driver (which binds them at
172
+ // runtime spawn). A switch on a member with a live runtime bound to a
173
+ // different model retires that runtime — the next round respawns onto the
174
+ // new model while the sub-dsh session itself resumes from disk.
175
+ const memberModelOverrides = new Map();
176
+ let currentLiveSwitch;
177
+ const modelBroker = new DshModelBroker({
178
+ ctx,
179
+ settingsModel: resolveModel,
180
+ cliDefault: hostDefaultModel,
181
+ defaultEffort: () => ctx.get('agentDefaultModel')?.currentSelection().reasoningEffort,
182
+ discovered: () => modelCatalog.read().entries.filter(entry => !entry.hidden).map(entry => entry.value),
183
+ catalog: modelCatalog,
184
+ recentModels: () => face.get().recentModels ?? [],
185
+ live: () => face.get().live,
186
+ overrides: memberModelOverrides,
187
+ liveBoundModel: childSessionId => currentLiveSwitch?.boundModel(childSessionId) ?? null,
188
+ retireRuntime: childSessionId => currentLiveSwitch?.retireRuntime(childSessionId) ?? Promise.resolve(),
189
+ });
190
+ /**
191
+ * This deployment's preset root — where a person or a pack installed the
192
+ * presets this instance offers, and the SOURCE every scope's own copy is
193
+ * taken from. Derived the way the roster derives it, from the settings
194
+ * service's harness home when there is one.
195
+ */
196
+ const instancePresetRoot = () => {
197
+ const home = ctx.get?.('settings')?.home;
198
+ return home === undefined ? defaultPresetRoot() : join(home, USER_PRESET_DIR);
199
+ };
200
+ const harness = {
201
+ prepareMember: async ({ binding, childSession, configuration, signal }) => {
202
+ const driver = currentLiveSwitch?.resolve(binding.childSessionId);
203
+ if (driver === undefined || driver.disabled)
204
+ throw new Error('Enable the native live harness before preparing a coordinator');
205
+ await driver.prepare({ cwd: binding.cwd, homeDir: ctx.localAgent.homeDir('dsh', binding.scope), childSession,
206
+ parentSessionId: binding.parentSessionId, configuration: configuration.resolved, sessionId: binding.childSessionId }, signal);
207
+ },
208
+ name: 'dsh',
209
+ displayName: 'dsh',
210
+ homeEnvVar: 'DSH_HOME',
211
+ delegationProvider: 'dsh-cli',
212
+ modelBroker,
213
+ records: { listSessions: homeDir => listDshSessions(homeDir) },
214
+ // A NAMED scope's directory is provisioned through this hook when the
215
+ // registry materializes it: the sub-profile the round launches from
216
+ // lives inside the scoped home, so a scope needs its own. (The
217
+ // credential does not: dsh authenticates through the host instance,
218
+ // which is why `isAuthenticated` below reads no directory at all.)
219
+ // `options.preset` is the per-condition input an evaluation's
220
+ // `conditions provision` hands in; the scope's own declaration answers
221
+ // for every other call, which is what makes a rostered preset survive
222
+ // a restart of this instance.
223
+ provision: (scopedHome, options) => {
224
+ const provisioned = provisionDshScope(scopedHome, config, {
225
+ ...(options?.preset === undefined ? {} : { preset: options.preset }),
226
+ presetRoot: instancePresetRoot(),
227
+ });
228
+ return {
229
+ ...(provisioned.preset === undefined ? {} : { preset: provisioned.preset }),
230
+ ...(provisioned.presetSnapshot === undefined ? {} : { presetSnapshot: provisioned.presetSnapshot }),
231
+ };
232
+ },
233
+ isAuthenticated: async () => {
234
+ try {
235
+ return (await resolveApiKey(ctx, config)) !== undefined;
236
+ }
237
+ catch {
238
+ return false;
239
+ }
240
+ },
241
+ // The eval snapshot. `sandbox` is the sub-profile's pinned permission
242
+ // boundary, reported exactly when one is configured: absence still
243
+ // means "this scope pins no boundary and runs what dsh-base composes",
244
+ // which is the honest condition-hash input for every deployment that
245
+ // never asked for one. Until T59 there was no knob to report at all,
246
+ // and the dsh rounds inside an evaluation unit paid for it — bash
247
+ // refused every call because the unit has no sandbox backend and a
248
+ // headless sub-dsh has no approval channel.
249
+ // The endpoint is the host instance's model config, which this provider
250
+ // never overrides, so no custom endpoint is ever pinned. The model is
251
+ // the host's default selection the sub-dsh inherits (the headless agent
252
+ // loader reads the same service), reported as `provider/model`; a
253
+ // composition without the service — or a selection that fails to read —
254
+ // reports no model rather than a guessed identifier.
255
+ // The directory is a PARAMETER, not a re-resolution of the default
256
+ // scope: an evaluation snapshot of a named scope must probe the CLI the
257
+ // way that scope's rounds launch it.
258
+ effectiveSettings: async (scopedHome) => {
259
+ const cliVersion = await dshCliVersion(ctx, config, scopedHome).catch(() => undefined);
260
+ // The family's fixed order: the plugin config key (it rides every
261
+ // launch as `--model`) before the host selection the sub-dsh would
262
+ // otherwise inherit.
263
+ const model = resolveModel() ?? hostDefaultModel();
264
+ return {
265
+ drive: face.get().live ? 'live' : 'exec',
266
+ baseUrlSet: false,
267
+ // dsh is the only harness that needs an extra node flag to run
268
+ // inside a container (undici does not read the proxy variables on
269
+ // its own). The provider injects it unconditionally on a container
270
+ // target, so the snapshot states it unconditionally — the condition
271
+ // file records the knob rather than leaving the fairest-to-compare
272
+ // difference between the four harnesses invisible.
273
+ containerNodeOptions: CONTAINER_NODE_OPTIONS,
274
+ // The CONFIGURED boundary, not a read-back of the sub-profile: this
275
+ // key is what provisioning writes, and it is in force for the very
276
+ // first round of a scope whose directory the hook has not
277
+ // materialized yet. Same shape as codex, which reports the sandbox
278
+ // policy its own config key puts on every argv.
279
+ ...config.permissions !== undefined ? { sandbox: config.permissions } : {},
280
+ ...model !== undefined ? { model } : {},
281
+ ...cliVersion !== undefined ? { cliVersion } : {},
282
+ };
283
+ },
284
+ };
285
+ const homeDir = ctx.localAgent.homeDir('dsh');
286
+ let generation = 0;
287
+ let currentEnabled;
288
+ const disposers = [];
289
+ const sync = (enabled) => {
290
+ // The watch fires on ANY namespace field change; only an enabled flip
291
+ // re-registers — the live fields ride the LiveDriverSwitch instead.
292
+ if (enabled === currentEnabled)
293
+ return;
294
+ currentEnabled = enabled;
295
+ generation += 1;
296
+ const gen = generation;
297
+ for (const dispose of disposers.splice(0))
298
+ dispose();
299
+ if (!enabled)
300
+ return;
301
+ // Idempotent: heals a deleted or drifted sub-profile before each round.
302
+ provisionDshScope(homeDir, config, { presetRoot: instancePresetRoot() });
303
+ disposers.push(ctx.localAgent.register(harness));
304
+ // The live driver is settings-driven WITHIN this enabled generation:
305
+ // the card's toggle (user layer over the YAML composition base) swaps
306
+ // driver generations without a reload. Toggling live OFF drains the
307
+ // retiring generation — new rounds fall back to exec, in-flight rounds
308
+ // finish on their runtime, idle runtimes are reclaimed at once. A
309
+ // legacy granularity setting no longer changes live behavior. Toggling ENABLED off keeps the historical hard semantics:
310
+ // provider unregisters and the switch disposes (disposeAll).
311
+ const liveSwitch = new LiveDriverSwitch(ctx, face, config, {
312
+ modelFor: childSessionId => memberModelOverrides.get(childSessionId) ?? resolveModel(),
313
+ });
314
+ currentLiveSwitch = liveSwitch;
315
+ disposers.push(ctx.subagents.registerProvider(new DshCliProvider(ctx, config, liveSwitch.resolve, resolveModel, childSessionId => memberModelOverrides.get(childSessionId))));
316
+ // The switch's disposal runs after the provider unregisters, so no
317
+ // in-flight round can re-spawn a runtime the teardown already reclaimed.
318
+ disposers.push(() => {
319
+ if (currentLiveSwitch === liveSwitch)
320
+ currentLiveSwitch = undefined;
321
+ liveSwitch.dispose();
322
+ });
323
+ // The family delegation tool is mounted dynamically so the toggle owns
324
+ // its lifecycle — while OFF the model never sees `subagent_dsh`.
325
+ void ctx.plugin(toolModule, { provider: 'dsh-cli', toolName: DSH_TOOL_NAME }).then((fiber) => {
326
+ if (gen !== generation) {
327
+ void fiber.dispose();
328
+ return;
329
+ }
330
+ disposers.push(() => { void fiber.dispose(); });
331
+ }, (error) => {
332
+ ctx.logger.warn(`local-agent-dsh: delegation tool mount failed: ${error instanceof Error ? error.message : String(error)}`);
333
+ });
334
+ };
335
+ sync(face.get().enabled);
336
+ const unwatch = face.watch((next) => { sync(next.enabled); });
337
+ return () => {
338
+ modelCatalog.dispose();
339
+ unwatch();
340
+ // Bump the generation so a tool mount still in flight disposes itself
341
+ // instead of being pushed onto a dead disposer list.
342
+ generation += 1;
343
+ for (const dispose of disposers.splice(0))
344
+ dispose();
345
+ };
346
+ }, 'local-agent-dsh: toggle controller');
347
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-local-agent-dsh`.
3
+ * @module @khorsheed/dsh-local-agent-dsh/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "local-agent-dsh-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - plugin context carrying the invariant registry.
13
+ * @returns the installed registration's disposer.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-local-agent-dsh`.
3
+ * @module @khorsheed/dsh-local-agent-dsh/invariant
4
+ */
5
+ const PACKAGE_NAME = '@khorsheed/dsh-local-agent-dsh';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'local-agent-dsh-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * The dsh harness definition is this bundle's contract with the core
12
+ * registry: whenever a `dsh` harness registers, its scoped-home variable must
13
+ * be the one this bundle pins, and it must delegate through the `dsh-cli`
14
+ * provider this bundle mounts.
15
+ */
16
+ const install = (ctx, fail) => {
17
+ ctx.on('internal/dispatch', (_mode, eventName, args) => {
18
+ if (eventName !== 'localAgent/harness-added')
19
+ return;
20
+ const harness = args[0];
21
+ if (harness.name !== 'dsh')
22
+ return;
23
+ if (harness.homeEnvVar !== 'DSH_HOME') {
24
+ fail('dsh harness registered with a home env other than the local-agent-dsh contract');
25
+ }
26
+ if (harness.delegationProvider !== 'dsh-cli') {
27
+ fail('dsh harness registered with a delegation provider other than the local-agent-dsh contract');
28
+ }
29
+ }, { global: true });
30
+ };
31
+ /**
32
+ * Register this package's invariant companion.
33
+ * @param ctx - plugin context carrying the invariant registry.
34
+ * @returns the installed registration's disposer.
35
+ */
36
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
37
+ /* jscpd:ignore-end */