@matamt-dbg/dsh-studio 0.2.1 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -62,6 +62,7 @@ var ROLE_TOOL = "subagent_role";
62
62
  var LEGACY_ROLE_TOOL = "studio_role";
63
63
  var GENERIC_DELEGATION_TOOLS = ["subagent", "subagent_fork", "list_subagent_models"];
64
64
  var SUBAGENT_PLUGIN = "@deepseek-ai/dsh-tool-subagent";
65
+ var isDelegationPlugin = (name) => name === SUBAGENT_PLUGIN || name === ROLE_PLUGIN;
65
66
  var WITHDRAWN_TOOLS = ["task_run"];
66
67
  function delegationTools(tools, hasRoles = false) {
67
68
  const delegates = hasRoles || tools.some((name) => name === ROLE_TOOL || name === LEGACY_ROLE_TOOL || GENERIC_DELEGATION_TOOLS.includes(name));
@@ -69,7 +70,7 @@ function delegationTools(tools, hasRoles = false) {
69
70
  return delegates ? [...rest, ROLE_TOOL] : rest;
70
71
  }
71
72
  function withoutDelegationPlugins(rows) {
72
- return rows.filter((row) => row.name !== SUBAGENT_PLUGIN && row.name !== ROLE_PLUGIN).map((row) => row.group && Array.isArray(row.config) ? { ...row, config: withoutDelegationPlugins(row.config) } : row);
73
+ return rows.filter((row) => !isDelegationPlugin(row.name)).map((row) => row.group && Array.isArray(row.config) ? { ...row, config: withoutDelegationPlugins(row.config) } : row);
73
74
  }
74
75
  function renameLegacyRoleKey(entries) {
75
76
  if (!entries || !Object.hasOwn(entries, LEGACY_ROLE_TOOL)) return entries;
@@ -91,7 +92,31 @@ function migratePolicyConfig(config) {
91
92
  // src/optimized-preset.ts
92
93
  var OPTIMIZED_PRESET_ID = "studio-optimized";
93
94
  var BUILTIN_TOOL_PRESETS = ["standard", "ptc", "minimal", "cordis"];
94
- var excluded = /* @__PURE__ */ new Set(["get_goal", "create_goal", "update_goal", "todo_write", "task_run", "cordis_inspect_list", "cordis_inspect_query", "plugin_manager"]);
95
+ var OPTIMIZED_TOOLS = [
96
+ "read_batch",
97
+ "read",
98
+ "write",
99
+ "edit",
100
+ "glob",
101
+ "grep",
102
+ "job_output",
103
+ "job_list",
104
+ "job_kill",
105
+ "skill",
106
+ "ask_user_question",
107
+ "web_search",
108
+ "web_fetch",
109
+ "present",
110
+ "bash",
111
+ "read_image",
112
+ "exit_plan_mode",
113
+ "send_message",
114
+ "interrupt_agent",
115
+ "list_agents",
116
+ "workflow",
117
+ "subagent_role"
118
+ ];
119
+ var ALLOWED_TOOLS = /* @__PURE__ */ new Set([...OPTIMIZED_TOOLS, ...GENERIC_DELEGATION_TOOLS, LEGACY_ROLE_TOOL]);
95
120
  var OPTIMIZED_PROMPT = `Use glob to discover paths, grep to search contents, and read to inspect files before editing. Prefer targeted edits.
96
121
  Batch independent read-only inspections with read_batch. Start independent tool calls together; keep calls that depend on earlier results sequential. Do not guess unknown arguments.
97
122
  Wait for required work to finish before validating it. Use background execution only for independent work or intentionally long-lived services. Do not duplicate running work or busy-poll it.
@@ -99,7 +124,7 @@ Track every job you start. Before reporting completion, collect all relevant res
99
124
  Delegate self-contained independent work when useful and collect the result before relying on it.
100
125
  Keep changes within the requested scope and preserve user work. Use the smallest sufficient checks for the requested outcome and risk, then stop when the work is complete.`;
101
126
  function optimizedTemplate(tools) {
102
- const selected = delegationTools(tools.filter((name) => !excluded.has(name)));
127
+ const selected = delegationTools(tools.filter((name) => ALLOWED_TOOLS.has(name)));
103
128
  const request = {
104
129
  preset: "studio-master",
105
130
  providers: ["@deepseek-ai/dsh-persona"],
@@ -378,7 +403,8 @@ var toolPromptSections = {
378
403
  web_search: ["tool:web_search"],
379
404
  get_goal: ["tool:goal"],
380
405
  create_goal: ["tool:goal"],
381
- update_goal: ["tool:goal"]
406
+ update_goal: ["tool:goal"],
407
+ workflow: ["tool:workflow"]
382
408
  };
383
409
 
384
410
  // src/preset-catalog.ts
@@ -491,6 +517,8 @@ function parsePreviewRequest(value) {
491
517
  if (row.providerConfigs !== void 0 && (!record4(row.providerConfigs) || Object.values(row.providerConfigs).some((v) => !record4(v)))) throw new Error("Invalid provider configs");
492
518
  if (row.configOverrides !== void 0) validateConfigOverrides(row.configOverrides);
493
519
  if (row.execution !== void 0) validateExecutionDefaults(row.execution);
520
+ const filter = row.subagentToolFilter;
521
+ if (filter !== void 0 && (!record4(filter) || !strings(filter.deny) || filter.deny.some((name) => !name))) throw new Error("Invalid subagent tool filter");
494
522
  return {
495
523
  ...row.configOverrides !== void 0 ? { configOverrides: row.configOverrides } : {},
496
524
  ...row.execution !== void 0 ? { execution: row.execution } : {},
@@ -502,7 +530,8 @@ function parsePreviewRequest(value) {
502
530
  ...row.persona !== void 0 ? { persona: row.persona } : {},
503
531
  ...row.sections !== void 0 ? { sections: row.sections } : {},
504
532
  ...row.toolOverrides !== void 0 ? { toolOverrides: row.toolOverrides } : {},
505
- ...row.providerConfigs !== void 0 ? { providerConfigs: row.providerConfigs } : {}
533
+ ...row.providerConfigs !== void 0 ? { providerConfigs: row.providerConfigs } : {},
534
+ ...filter !== void 0 && filter.deny.length ? { subagentToolFilter: { deny: [...new Set(filter.deny)] } } : {}
506
535
  };
507
536
  }
508
537
  function promptProviders(rows) {
@@ -537,12 +566,26 @@ function parseComposition(content) {
537
566
  function findProviders(rows) {
538
567
  return rows.flatMap((row) => row.disabled === true ? [] : row.group && Array.isArray(row.config) ? findProviders(row.config) : promptProviderModules.includes(row.name) ? [row] : []);
539
568
  }
569
+ function normalizeRetention(config, overrides) {
570
+ if (overrides?.retainTokens !== void 0 && overrides.retainRatio !== void 0) {
571
+ throw new Error("Compaction retainRatio and retainTokens are mutually exclusive");
572
+ }
573
+ if (overrides?.retainTokens !== void 0) delete config.retainRatio;
574
+ if (overrides?.retainRatio !== void 0) delete config.retainTokens;
575
+ }
540
576
  async function compositionPlan(ctx, input) {
541
577
  const request = parsePreviewRequest(input);
542
- const base = request.preset === MASTER_PRESET_ID ? MASTER_PRESET_ID : "minimal";
578
+ const base = request.preset;
543
579
  if (request.configOverrides && base !== MASTER_PRESET_ID) throw new Error("Config overrides require Studio Master");
544
- const minimal = applyConfigOverrides(parseComposition((await ctx.agentPresets.readDocument(base)).content), request.configOverrides ?? {});
545
- const available = new Map(findProviders(minimal).map((row) => [row.name, row]));
580
+ const source = applyConfigOverrides(parseComposition((await ctx.agentPresets.readDocument(base)).content), request.configOverrides ?? {});
581
+ if (request.configOverrides && Object.keys(request.configOverrides).length) {
582
+ for (const { id, row } of configEntries(source)) {
583
+ if (row.name === "@deepseek-ai/dsh-compaction-basic") {
584
+ normalizeRetention(row.config, request.configOverrides[id]);
585
+ }
586
+ }
587
+ }
588
+ const available = new Map(findProviders(source).map((row) => [row.name, row]));
546
589
  const presets = await ctx.agentPresets.list();
547
590
  for (const preset of presets) {
548
591
  if (base === MASTER_PRESET_ID || !["minimal", "standard", "ptc", "cordis"].includes(preset.id)) continue;
@@ -572,33 +615,21 @@ async function compositionPlan(ctx, input) {
572
615
  if (request.persona !== void 0) config.prefix = request.persona;
573
616
  }
574
617
  if (name === "@deepseek-ai/dsh-compaction-basic") {
575
- if (overrides?.retainTokens !== void 0 && overrides.retainRatio === void 0) delete config.retainRatio;
576
- if (overrides?.retainRatio !== void 0 && overrides.retainTokens === void 0) delete config.retainTokens;
618
+ normalizeRetention(config, overrides);
577
619
  }
578
620
  return { ...row, id: `studio-provider-${index}`, config };
579
621
  });
580
622
  if (request.persona !== void 0 && !request.providers.includes("@deepseek-ai/dsh-persona")) throw new Error("Select persona before editing its prefix");
581
623
  for (const name of Object.keys(request.providerConfigs ?? {})) if (!request.providers.includes(name)) throw new Error(`Unselected provider override: ${name}`);
582
- const globalNames = new Set(ctx.tools.schemas().map((tool) => tool.name));
583
- const missing = request.tools.filter((name) => !globalNames.has(name));
584
- if (missing.length || base === MASTER_PRESET_ID) {
585
- let source;
586
- const candidates = base === MASTER_PRESET_ID ? [MASTER_PRESET_ID] : ["minimal", ...presets.map((p) => p.id).filter((id) => id !== "minimal" && !id.startsWith("studio-"))];
587
- for (const id of candidates) {
588
- let lease;
589
- try {
590
- lease = await ctx.agentPresets.acquireScope(id);
591
- const names2 = new Set(ctx.tools.schemas(lease.key).map((tool) => tool.name));
592
- if (missing.every((name) => names2.has(name))) {
593
- source = id === base ? minimal : parseComposition((await ctx.agentPresets.readDocument(id)).content);
594
- break;
595
- }
596
- } catch {
597
- } finally {
598
- await lease?.[Symbol.asyncDispose]();
599
- }
624
+ if (request.tools.length || base === MASTER_PRESET_ID) {
625
+ const lease = await ctx.agentPresets.acquireScope(base);
626
+ try {
627
+ const names2 = new Set(ctx.tools.schemas(lease.key).map((tool) => tool.name));
628
+ const missing = request.tools.filter((name) => !names2.has(name));
629
+ if (missing.length) throw new Error(`Requested preset ${base} does not supply tools: ${missing.join(", ")}. Tools from other source scopes cannot be substituted safely.`);
630
+ } finally {
631
+ await lease[Symbol.asyncDispose]();
600
632
  }
601
- if (!source) throw new Error(`No compatible installed tool composition supplies: ${missing.join(", ")}. Tools from incompatible source scopes cannot be combined safely.`);
602
633
  plugins.push(...selectPromptProviders(source, []));
603
634
  }
604
635
  const compaction = plugins.filter((row) => ["@deepseek-ai/dsh-compaction-basic", "@deepseek-ai/dsh-compaction-tool-result-pruner"].includes(row.name));
@@ -608,7 +639,7 @@ async function compositionPlan(ctx, input) {
608
639
  }
609
640
 
610
641
  // src/host/preset-policy.ts
611
- import { scopeOf } from "@deepseek-ai/dsh-scope";
642
+ import { scopeChainOf, scopeOf } from "@deepseek-ai/dsh-scope";
612
643
  var POLICY_SECTION_ORDERS = Object.freeze({
613
644
  "tool:jobs": "TOOL_JOBS",
614
645
  "tool:bash": "TOOL_BASH",
@@ -620,7 +651,8 @@ var POLICY_SECTION_ORDERS = Object.freeze({
620
651
  "tool:grep": "TOOL_GREP",
621
652
  "tool:web_fetch": "TOOL_WEB_FETCH",
622
653
  "tool:web_search": "TOOL_WEB_SEARCH",
623
- "tool:goal": "TOOL_GOAL"
654
+ "tool:goal": "TOOL_GOAL",
655
+ "tool:workflow": "TOOL_WORKFLOW"
624
656
  });
625
657
  var own2 = (object3, key) => Object.prototype.hasOwnProperty.call(object3, key);
626
658
  var record3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
@@ -638,6 +670,11 @@ function validatePolicy(config) {
638
670
  for (const [name, text] of Object.entries(config.sections)) {
639
671
  if (!own2(POLICY_SECTION_ORDERS, name) || typeof text !== "string") throw new Error(`Unsupported policy section: ${name}`);
640
672
  }
673
+ if (config.childToolFilter !== void 0) {
674
+ const deny = record3(config.childToolFilter) ? config.childToolFilter.deny : void 0;
675
+ if (!Array.isArray(deny) || deny.some((name) => typeof name !== "string" || !name)) throw new Error("Invalid subagent tool filter");
676
+ for (const name of deny) if (!config.tools.includes(name)) throw new Error(`Subagent tool filter requires selected tool: ${name}`);
677
+ }
641
678
  }
642
679
  function patchParameter(parameters, path, description) {
643
680
  const segments = path.split(".");
@@ -651,10 +688,21 @@ function patchParameter(parameters, path, description) {
651
688
  function installPolicy(ctx, scopeKey, config) {
652
689
  validatePolicy(config);
653
690
  if (scopeOf(ctx) !== scopeKey) throw new Error("Studio policy requires its owning scoped context");
691
+ if (config.tools.includes("run_code") || ctx.tools.get("run_code", scopeKey) !== void 0) {
692
+ throw new Error("Studio policy does not support PTC presentation or the reserved run_code tool; configure this preset with native tool presentation");
693
+ }
694
+ const missing = config.tools.filter((name) => ctx.tools.get(name, scopeKey) === void 0);
695
+ if (missing.length) throw new Error(`Studio policy selected tools unavailable: ${missing.join(", ")}`);
696
+ const restoreProbe = ctx.tools.restrict({ allow: [] });
697
+ try {
698
+ const local = ctx.tools.schemas(scopeKey);
699
+ if (local.length) throw new Error(`Studio policy does not support own-scope tools: ${local.map((tool) => tool.name).join(", ")}; register providers in an ancestor preset scope. Public API cannot hide late own-scope schemas; dynamic own-scope providers are unsupported`);
700
+ } finally {
701
+ restoreProbe();
702
+ }
654
703
  const selected = new Set(config.tools);
655
704
  const definitions = config.tools.map((name) => {
656
705
  const definition = ctx.tools.get(name, scopeKey);
657
- if (!definition) throw new Error(`Unavailable selected tool: ${name}`);
658
706
  const override = config.toolOverrides[name];
659
707
  const defaults = config.execution?.defaults[name];
660
708
  const batch = config.execution?.batch?.tool === name ? config.execution.batch : void 0;
@@ -687,9 +735,7 @@ function installPolicy(ctx, scopeKey, config) {
687
735
  for (const release of disposers.splice(0).reverse()) release();
688
736
  };
689
737
  try {
690
- disposers.push(ctx.tools.restrict({ allow: config.tools }));
691
- const excess = ctx.tools.schemas(scopeKey).filter((tool) => !selected.has(tool.name));
692
- if (excess.length) throw new Error(`Unselected scoped tools must be removed from preset plugins: ${excess.map((tool) => tool.name).join(", ")}`);
738
+ disposers.push(ctx.tools.restrict({ allow: [...selected] }));
693
739
  disposers.push(ctx.tools.guard((execution) => selected.has(execution.name) ? void 0 : "Tool excluded by Studio preset policy"));
694
740
  for (const definition of definitions) disposers.push(ctx.tools.register(definition));
695
741
  for (const [name, text] of Object.entries(config.sections)) disposers.push(ctx.systemPrompt.section({ name, order: ctx.systemPrompt.getSectionOrder(POLICY_SECTION_ORDERS[name]), text, interpolate: false }));
@@ -756,6 +802,7 @@ function executionCapabilities(tools) {
756
802
  }
757
803
 
758
804
  // src/host/preset-store.ts
805
+ import { createScope as createScope2 } from "@deepseek-ai/dsh-scope";
759
806
  import { randomUUID as randomUUID3 } from "node:crypto";
760
807
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, rename as rename2 } from "node:fs/promises";
761
808
  import { dirname as dirname2, join } from "node:path";
@@ -785,11 +832,27 @@ function parseRoles(input) {
785
832
  if (!/^[a-zA-Z0-9_-]+$/.test(reasoning)) throw new Error("Role reasoning must be inherit or an effort identifier");
786
833
  if (spawn !== "fresh" && spawn !== "fork") throw new Error("Role spawn must be fresh or fork");
787
834
  seen.add(role);
788
- return { name, role, model, reasoning, spawn, persona, ..."overrides" in value ? { overrides: structuredClone(value.overrides) } : {} };
835
+ const filter = value.toolFilter;
836
+ if (filter !== void 0 && (!object2(filter) || !Array.isArray(filter.deny) || filter.deny.some((name2) => typeof name2 !== "string" || !name2) || filter.mode !== void 0 && filter.mode !== "extend" && filter.mode !== "replace")) throw new Error("Invalid role tool filter");
837
+ return {
838
+ name,
839
+ role,
840
+ model,
841
+ reasoning,
842
+ spawn,
843
+ persona,
844
+ ...filter !== void 0 ? { toolFilter: { mode: filter.mode ?? "replace", deny: [...new Set(filter.deny)] } } : {},
845
+ ..."overrides" in value ? { overrides: structuredClone(value.overrides) } : {}
846
+ };
789
847
  });
790
848
  }
791
849
  function executableRoles(roles) {
792
- return roles.map(({ name, role, model, reasoning, spawn, persona }) => ({ name, role, model, reasoning, spawn, persona }));
850
+ return roles.map(({ name, role, model, reasoning, spawn, persona, toolFilter }) => ({ name, role, model, reasoning, spawn, persona, ...toolFilter ? { toolFilter } : {} }));
851
+ }
852
+ function roleToolFilter(role, presetDefault = []) {
853
+ const own3 = role?.toolFilter;
854
+ if (!own3) return [...presetDefault];
855
+ return own3.mode === "replace" ? [...own3.deny] : [.../* @__PURE__ */ new Set([...presetDefault, ...own3.deny])];
793
856
  }
794
857
 
795
858
  // src/host/role-validation.ts
@@ -819,7 +882,7 @@ async function validateRoleModel(llm, role, signal) {
819
882
 
820
883
  // src/host/preset-store.ts
821
884
  var owned = (id) => id === OPTIMIZED_PRESET_ID || /^studio-user-[a-f0-9-]{36}$/.test(id);
822
- var warning = "Work convenience settings are saved but not applied yet. Legacy role overrides are stored only; roles apply only their supported settings.";
885
+ var retiredLegacyWarning = "Work convenience settings are saved but not applied yet. Legacy role overrides are stored only; roles apply only their supported settings.";
823
886
  function migrateLegacyNames(row) {
824
887
  const request = row.request;
825
888
  if (request.toolOverrides) {
@@ -862,6 +925,7 @@ var PresetStore = class {
862
925
  if (!owned(row.id)) throw new Error("Invalid Studio preset ownership");
863
926
  parsePreviewRequest(row.request);
864
927
  migrateLegacyNames(row);
928
+ if (row.applyWarning === retiredLegacyWarning) delete row.applyWarning;
865
929
  }
866
930
  } catch (error) {
867
931
  if (error.code !== "ENOENT") throw error;
@@ -877,10 +941,32 @@ var PresetStore = class {
877
941
  })();
878
942
  }
879
943
  async mount(id, definition) {
880
- const dispose = await this.ctx.agentPresets.register({ id, name: definition.name, description: definition.description, plugins: parseComposition(definition.plugins) });
944
+ const plugins = parseComposition(definition.plugins);
945
+ const dispose = await this.ctx.agentPresets.register({ id, name: definition.name, description: definition.description, plugins });
881
946
  try {
882
947
  const preset = await this.ctx.agentPresets.resolve(id);
883
948
  if (preset.broken) throw new Error(preset.broken);
949
+ const policies = (rows) => rows.flatMap((row) => row.disabled === true ? [] : row.group && Array.isArray(row.config) ? policies(row.config) : row.name === POLICY_PLUGIN ? [row.config] : []);
950
+ const configs = policies(plugins);
951
+ if (configs.length) {
952
+ const lease = await this.ctx.agentPresets.acquireScope(id);
953
+ try {
954
+ const key = {};
955
+ const child = createScope2(this.ctx, key, { parent: lease.key });
956
+ const releases = [];
957
+ try {
958
+ for (const config of configs) releases.push(installPolicy(child.ctx, key, config));
959
+ } finally {
960
+ try {
961
+ for (const release of releases.reverse()) release();
962
+ } finally {
963
+ await child.dispose();
964
+ }
965
+ }
966
+ } finally {
967
+ await lease[Symbol.asyncDispose]();
968
+ }
969
+ }
884
970
  this.disposers.set(id, dispose);
885
971
  } catch (error) {
886
972
  await dispose();
@@ -907,27 +993,72 @@ var PresetStore = class {
907
993
  const { applied, ...value } = row;
908
994
  return structuredClone({ ...value, ...row.id === OPTIMIZED_PRESET_ID ? { template: true, nonDeletable: true } : {} });
909
995
  }
910
- /** Seed once. Later startups retain user edits and explicit unapplied state. */
996
+ /** Compose the Optimized applied snapshot: the template request over the current base composition. */
997
+ optimizedApplied(request, plugins) {
998
+ const delegates = request.tools.includes(ROLE_TOOL);
999
+ const base = applyConfigOverrides(plugins, request.configOverrides ?? {});
1000
+ return dump([...delegates ? [...withoutDelegationPlugins(base), { id: "studio-roles", name: ROLE_PLUGIN, config: { roles: [] } }] : base, { id: "studio-optimized-policy", name: POLICY_PLUGIN, config: { tools: request.tools, toolOverrides: request.toolOverrides, sections: {}, prompt: request.prompt, execution: request.execution } }], { schema: entryListSchema2, noRefs: true });
1001
+ }
1002
+ /**
1003
+ * Seed the built-in on first run, then keep it aligned with the shipped template. Optimized is
1004
+ * locked against editing, so its stored request carries no user content — only the applied or
1005
+ * unapplied choice is the user's, and that survives. Without the alignment a profile plugin that
1006
+ * registers a new tool would enlarge the preset forever after its first seed.
1007
+ */
911
1008
  ensureOptimized(tools, plugins) {
912
1009
  return this.serial(async () => {
913
- const existing = this.rows.find((row2) => row2.id === OPTIMIZED_PRESET_ID);
914
- if (existing) return this.public(existing);
915
1010
  const template = optimizedTemplate(tools);
916
- const request = template.request;
917
- const delegates = request.tools.includes(ROLE_TOOL);
918
- const base = applyConfigOverrides(plugins, request.configOverrides ?? {});
919
- const applied = { name: template.name, description: template.description, plugins: dump([...delegates ? [...withoutDelegationPlugins(base), { id: "studio-roles", name: ROLE_PLUGIN, config: { roles: [] } }] : base, { id: "studio-optimized-policy", name: POLICY_PLUGIN, config: { tools: request.tools, toolOverrides: request.toolOverrides, sections: {}, prompt: request.prompt, execution: request.execution } }], { schema: entryListSchema2, noRefs: true }) };
1011
+ const existing = this.rows.find((row) => row.id === OPTIMIZED_PRESET_ID);
1012
+ if (existing && JSON.stringify(existing.request) === JSON.stringify(template.request)) return this.public(existing);
920
1013
  const now = (/* @__PURE__ */ new Date()).toISOString();
921
- const row = { id: template.id, name: template.name, description: template.description, request: template.request, conveniences: {}, agents: [], updatedAt: now, appliedAt: now, status: "applied", applied, ...template.applyWarning ? { applyWarning: template.applyWarning } : {} };
922
- await this.mount(row.id, applied);
1014
+ const buildApplied = () => ({ name: template.name, description: template.description, plugins: this.optimizedApplied(template.request, plugins) });
1015
+ if (!existing) {
1016
+ const applied = buildApplied();
1017
+ const row = { id: template.id, name: template.name, description: template.description, request: template.request, conveniences: {}, agents: [], updatedAt: now, appliedAt: now, status: "applied", applied, ...template.applyWarning ? { applyWarning: template.applyWarning } : {} };
1018
+ await this.mount(row.id, applied);
1019
+ try {
1020
+ await this.persist([...this.rows, row]);
1021
+ } catch (error) {
1022
+ await this.disposers.get(row.id)?.();
1023
+ this.disposers.delete(row.id);
1024
+ throw error;
1025
+ }
1026
+ return this.public(row);
1027
+ }
1028
+ const refresh = { ...existing, name: template.name, description: template.description, request: template.request, updatedAt: now };
1029
+ if (template.applyWarning) refresh.applyWarning = template.applyWarning;
1030
+ else delete refresh.applyWarning;
1031
+ if (existing.applied) {
1032
+ const applied = buildApplied();
1033
+ await this.disposers.get(existing.id)?.();
1034
+ this.disposers.delete(existing.id);
1035
+ try {
1036
+ await this.mount(existing.id, applied);
1037
+ } catch (error) {
1038
+ try {
1039
+ await this.mount(existing.id, existing.applied);
1040
+ } catch {
1041
+ }
1042
+ throw error;
1043
+ }
1044
+ refresh.applied = applied;
1045
+ refresh.appliedAt = now;
1046
+ refresh.status = "applied";
1047
+ }
923
1048
  try {
924
- await this.persist([...this.rows, row]);
1049
+ await this.persist(this.rows.map((row) => row.id === existing.id ? refresh : row));
925
1050
  } catch (error) {
926
- await this.disposers.get(row.id)?.();
927
- this.disposers.delete(row.id);
1051
+ await this.disposers.get(existing.id)?.();
1052
+ this.disposers.delete(existing.id);
1053
+ if (existing.applied) {
1054
+ try {
1055
+ await this.mount(existing.id, existing.applied);
1056
+ } catch {
1057
+ }
1058
+ }
928
1059
  throw error;
929
1060
  }
930
- return this.public(row);
1061
+ return this.public(refresh);
931
1062
  });
932
1063
  }
933
1064
  list() {
@@ -972,21 +1103,25 @@ var PresetStore = class {
972
1103
  for (const role of roles) await validateRoleModel(this.ctx.get("llm"), role);
973
1104
  const delegates = tools.includes(ROLE_TOOL);
974
1105
  const keep = (entries) => Object.fromEntries(Object.entries(entries).filter(([name]) => tools.includes(name)));
975
- const rolePlugins = delegates ? [{ id: "studio-roles", name: ROLE_PLUGIN, config: { roles } }] : [];
1106
+ const presetHidden = old.request.subagentToolFilter?.deny ?? [];
1107
+ const lists = [presetHidden, ...roles.map((role) => roleToolFilter(role, presetHidden))];
1108
+ const unknownHidden = [...new Set(lists.flat().filter((name) => !tools.includes(name) || name === ROLE_TOOL))];
1109
+ if (delegates && unknownHidden.length) throw new Error(`Subagent Tool Filter may only list tools selected in this preset: ${unknownHidden.join(", ")}`);
1110
+ const hideable = delegates ? [...new Set(lists.flat())] : [];
1111
+ const childToolFilter = hideable.length ? { deny: hideable } : void 0;
1112
+ const rolePlugins = delegates ? [{ id: "studio-roles", name: ROLE_PLUGIN, config: { roles, ...presetHidden.length ? { toolFilter: { deny: [...presetHidden] } } : {} } }] : [];
976
1113
  const execution = request.execution && {
977
1114
  ...request.execution,
978
1115
  defaults: keep(request.execution.defaults),
979
1116
  ...request.execution.batch && !tools.includes(request.execution.batch.tool) ? { batch: void 0 } : {}
980
1117
  };
981
- const applied = { name: old.name, description: old.description, plugins: dump([...withoutDelegationPlugins(plugins), ...rolePlugins, { id: "studio-policy", name: POLICY_PLUGIN, config: { tools: [...new Set(tools)], toolOverrides: keep(request.toolOverrides ?? {}), sections: request.sections ?? {}, prompt: request.prompt, ...execution ? { execution } : {} } }], { schema: entryListSchema2, noRefs: true }) };
1118
+ const applied = { name: old.name, description: old.description, plugins: dump([...withoutDelegationPlugins(plugins), ...rolePlugins, { id: "studio-policy", name: POLICY_PLUGIN, config: { tools: [...new Set(tools)], toolOverrides: keep(request.toolOverrides ?? {}), sections: request.sections ?? {}, prompt: request.prompt, ...execution ? { execution } : {}, ...childToolFilter ? { childToolFilter } : {} } }], { schema: entryListSchema2, noRefs: true }) };
982
1119
  await this.disposers.get(id)?.();
983
1120
  this.disposers.delete(id);
984
1121
  try {
985
1122
  await this.mount(id, applied);
986
- const hasLegacySettings = old.conveniences != null && Object.keys(Object(old.conveniences)).length > 0 || Array.isArray(old.agents) && old.agents.some((role) => role && Object.hasOwn(role, "overrides"));
987
1123
  const row = { ...old, applied, appliedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "applied" };
988
- if (hasLegacySettings) row.applyWarning = warning;
989
- else delete row.applyWarning;
1124
+ delete row.applyWarning;
990
1125
  await this.persist(this.rows.map((item) => item.id === id ? row : item));
991
1126
  return this.public(row);
992
1127
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matamt-dbg/dsh-studio",
3
- "version": "0.2.1",
3
+ "version": "0.2.7",
4
4
  "description": "DSH Studio: build, edit and apply DeepSeek Harness agent presets — tools, prompts, Configs and role-based subagents — from the web GUI.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
package/policy.js CHANGED
@@ -39,7 +39,34 @@ function applyExecutionDefaults(args, defaults) {
39
39
  }
40
40
 
41
41
  // src/host/preset-policy.ts
42
- import { scopeOf } from "@deepseek-ai/dsh-scope";
42
+ import { scopeChainOf, scopeOf } from "@deepseek-ai/dsh-scope";
43
+
44
+ // src/tool-prompt-sections.ts
45
+ var toolPromptSections = {
46
+ bash: ["tool:bash"],
47
+ pwsh: ["tool:pwsh"],
48
+ read: ["tool:read"],
49
+ write: ["tool:write"],
50
+ edit: ["tool:edit"],
51
+ glob: ["tool:glob"],
52
+ grep: ["tool:grep"],
53
+ job_list: ["tool:jobs"],
54
+ job_output: ["tool:jobs"],
55
+ job_kill: ["tool:jobs"],
56
+ web_fetch: ["tool:web_fetch"],
57
+ web_search: ["tool:web_search"],
58
+ get_goal: ["tool:goal"],
59
+ create_goal: ["tool:goal"],
60
+ update_goal: ["tool:goal"],
61
+ workflow: ["tool:workflow"]
62
+ };
63
+ function sectionsOnlyFor(hidden, kept) {
64
+ const owned = (names2) => new Set(names2.flatMap((name2) => toolPromptSections[name2] ?? []));
65
+ const still = owned(kept);
66
+ return [...owned(hidden)].filter((section) => !still.has(section));
67
+ }
68
+
69
+ // src/host/preset-policy.ts
43
70
  var POLICY_SECTION_ORDERS = Object.freeze({
44
71
  "tool:jobs": "TOOL_JOBS",
45
72
  "tool:bash": "TOOL_BASH",
@@ -51,7 +78,8 @@ var POLICY_SECTION_ORDERS = Object.freeze({
51
78
  "tool:grep": "TOOL_GREP",
52
79
  "tool:web_fetch": "TOOL_WEB_FETCH",
53
80
  "tool:web_search": "TOOL_WEB_SEARCH",
54
- "tool:goal": "TOOL_GOAL"
81
+ "tool:goal": "TOOL_GOAL",
82
+ "tool:workflow": "TOOL_WORKFLOW"
55
83
  });
56
84
  var own2 = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
57
85
  var record2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
@@ -69,6 +97,26 @@ function validatePolicy(config) {
69
97
  for (const [name2, text] of Object.entries(config.sections)) {
70
98
  if (!own2(POLICY_SECTION_ORDERS, name2) || typeof text !== "string") throw new Error(`Unsupported policy section: ${name2}`);
71
99
  }
100
+ if (config.childToolFilter !== void 0) {
101
+ const deny = record2(config.childToolFilter) ? config.childToolFilter.deny : void 0;
102
+ if (!Array.isArray(deny) || deny.some((name2) => typeof name2 !== "string" || !name2)) throw new Error("Invalid subagent tool filter");
103
+ for (const name2 of deny) if (!config.tools.includes(name2)) throw new Error(`Subagent tool filter requires selected tool: ${name2}`);
104
+ }
105
+ }
106
+ function childPolicy(config, hidden) {
107
+ const deny = new Set(hidden);
108
+ if (!deny.size) return config;
109
+ const keep = (entries) => Object.fromEntries(Object.entries(entries).filter(([name2]) => !deny.has(name2)));
110
+ const { childToolFilter: _filter, execution, ...rest } = config;
111
+ const tools = config.tools.filter((name2) => !deny.has(name2));
112
+ const blank = Object.fromEntries(sectionsOnlyFor([...deny], tools).filter((name2) => own2(POLICY_SECTION_ORDERS, name2)).map((name2) => [name2, ""]));
113
+ return {
114
+ ...rest,
115
+ tools,
116
+ toolOverrides: keep(config.toolOverrides),
117
+ sections: { ...config.sections, ...blank },
118
+ ...execution ? { execution: (({ batch, ...value }) => ({ ...value, defaults: keep(execution.defaults), ...batch && !deny.has(batch.tool) ? { batch } : {} }))(execution) } : {}
119
+ };
72
120
  }
73
121
  function patchParameter(parameters, path, description) {
74
122
  const segments = path.split(".");
@@ -82,10 +130,21 @@ function patchParameter(parameters, path, description) {
82
130
  function installPolicy(ctx, scopeKey, config) {
83
131
  validatePolicy(config);
84
132
  if (scopeOf(ctx) !== scopeKey) throw new Error("Studio policy requires its owning scoped context");
133
+ if (config.tools.includes("run_code") || ctx.tools.get("run_code", scopeKey) !== void 0) {
134
+ throw new Error("Studio policy does not support PTC presentation or the reserved run_code tool; configure this preset with native tool presentation");
135
+ }
136
+ const missing = config.tools.filter((name2) => ctx.tools.get(name2, scopeKey) === void 0);
137
+ if (missing.length) throw new Error(`Studio policy selected tools unavailable: ${missing.join(", ")}`);
138
+ const restoreProbe = ctx.tools.restrict({ allow: [] });
139
+ try {
140
+ const local = ctx.tools.schemas(scopeKey);
141
+ if (local.length) throw new Error(`Studio policy does not support own-scope tools: ${local.map((tool) => tool.name).join(", ")}; register providers in an ancestor preset scope. Public API cannot hide late own-scope schemas; dynamic own-scope providers are unsupported`);
142
+ } finally {
143
+ restoreProbe();
144
+ }
85
145
  const selected = new Set(config.tools);
86
146
  const definitions = config.tools.map((name2) => {
87
147
  const definition = ctx.tools.get(name2, scopeKey);
88
- if (!definition) throw new Error(`Unavailable selected tool: ${name2}`);
89
148
  const override = config.toolOverrides[name2];
90
149
  const defaults = config.execution?.defaults[name2];
91
150
  const batch = config.execution?.batch?.tool === name2 ? config.execution.batch : void 0;
@@ -118,9 +177,7 @@ function installPolicy(ctx, scopeKey, config) {
118
177
  for (const release of disposers.splice(0).reverse()) release();
119
178
  };
120
179
  try {
121
- disposers.push(ctx.tools.restrict({ allow: config.tools }));
122
- const excess = ctx.tools.schemas(scopeKey).filter((tool) => !selected.has(tool.name));
123
- if (excess.length) throw new Error(`Unselected scoped tools must be removed from preset plugins: ${excess.map((tool) => tool.name).join(", ")}`);
180
+ disposers.push(ctx.tools.restrict({ allow: [...selected] }));
124
181
  disposers.push(ctx.tools.guard((execution) => selected.has(execution.name) ? void 0 : "Tool excluded by Studio preset policy"));
125
182
  for (const definition of definitions) disposers.push(ctx.tools.register(definition));
126
183
  for (const [name2, text] of Object.entries(config.sections)) disposers.push(ctx.systemPrompt.section({ name: name2, order: ctx.systemPrompt.getSectionOrder(POLICY_SECTION_ORDERS[name2]), text, interpolate: false }));
@@ -135,23 +192,91 @@ var name = "studio-preset-policy";
135
192
  var inject = ["tools", "systemPrompt"];
136
193
  function apply(ctx, config) {
137
194
  validatePolicy(config);
195
+ const registry = ctx.get("agents");
196
+ const hideable = config.childToolFilter?.deny ?? [];
197
+ const policyFor = (agent) => {
198
+ if (!hideable.length) return config;
199
+ const provided = (name2) => scopeOf(ctx) === void 0 || ctx.tools.get(name2, scopeOf(ctx)) !== void 0;
200
+ const hidden = hideable.filter((name2) => agent.ctx.tools.get(name2, agent) === void 0 && provided(name2));
201
+ return hidden.length ? childPolicy(config, hidden) : config;
202
+ };
138
203
  const installed = /* @__PURE__ */ new Map();
204
+ const installing = /* @__PURE__ */ new Set();
205
+ const install = (agent) => {
206
+ if (installed.has(agent) || installing.has(agent)) return;
207
+ installing.add(agent);
208
+ try {
209
+ installed.set(agent, installPolicy(agent.ctx, agent, policyFor(agent)));
210
+ } finally {
211
+ installing.delete(agent);
212
+ }
213
+ };
214
+ const remove = (agent) => {
215
+ installed.get(agent)?.();
216
+ installed.delete(agent);
217
+ };
139
218
  ctx.on("agent/created", ({ agent }) => {
140
- if (!installed.has(agent)) installed.set(agent, installPolicy(agent.ctx, agent, config));
219
+ install(agent);
141
220
  return void 0;
142
221
  });
143
222
  ctx.on("agent/disposed", ({ agent }) => {
144
- installed.get(agent)?.();
145
- installed.delete(agent);
223
+ remove(agent);
224
+ });
225
+ const composition = scopeOf(ctx);
226
+ const agents = registry;
227
+ if (composition === void 0 || agents === void 0) {
228
+ ctx.effect(() => () => {
229
+ for (const dispose of installed.values()) dispose();
230
+ installed.clear();
231
+ });
232
+ return;
233
+ }
234
+ const belongs = (agent) => scopeChainOf(scopeOf(agent.ctx)).includes(composition);
235
+ const blocked = /* @__PURE__ */ new Map();
236
+ const unblock = (agent) => {
237
+ blocked.get(agent)?.release();
238
+ blocked.delete(agent);
239
+ };
240
+ const block = (agent, error) => {
241
+ const reason = `Studio preset policy could not be installed: ${error instanceof Error ? error.message : String(error)}`;
242
+ const current = blocked.get(agent);
243
+ if (current?.reason === reason) return;
244
+ current?.release();
245
+ blocked.set(agent, { release: agent.ctx.tools.guard(() => reason), reason });
246
+ };
247
+ const reconcile = () => {
248
+ const live = agents.list();
249
+ for (const agent of live) {
250
+ if (!belongs(agent)) {
251
+ remove(agent);
252
+ unblock(agent);
253
+ continue;
254
+ }
255
+ if (installed.has(agent)) continue;
256
+ try {
257
+ install(agent);
258
+ unblock(agent);
259
+ } catch (error) {
260
+ block(agent, error);
261
+ }
262
+ }
263
+ for (const agent of [...blocked.keys()]) if (!live.includes(agent)) unblock(agent);
264
+ };
265
+ ctx.on("tools/change", reconcile);
266
+ ctx.on("agent/disposed", ({ agent }) => {
267
+ unblock(agent);
146
268
  });
269
+ reconcile();
147
270
  ctx.effect(() => () => {
148
271
  for (const dispose of installed.values()) dispose();
149
272
  installed.clear();
273
+ for (const agent of [...blocked.keys()]) unblock(agent);
150
274
  });
151
275
  }
152
276
  export {
153
277
  POLICY_SECTION_ORDERS,
154
278
  apply,
279
+ childPolicy,
155
280
  inject,
156
281
  installPolicy,
157
282
  name,