@matamt-dbg/dsh-studio 0.2.2 → 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.
Files changed (6) hide show
  1. package/README.md +9 -4
  2. package/client.js +281 -174
  3. package/index.js +193 -60
  4. package/package.json +1 -1
  5. package/policy.js +135 -12
  6. package/roles.js +25 -3
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,11 +688,20 @@ 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
+ }
654
694
  const missing = config.tools.filter((name) => ctx.tools.get(name, scopeKey) === void 0);
655
- if (missing.length) console.warn(`Studio policy: ${missing.length} selected tool(s) unavailable on this host, skipped: ${missing.join(", ")}`);
656
- const available = config.tools.filter((name) => !missing.includes(name));
657
- const selected = new Set(available);
658
- const definitions = available.map((name) => {
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
+ }
703
+ const selected = new Set(config.tools);
704
+ const definitions = config.tools.map((name) => {
659
705
  const definition = ctx.tools.get(name, scopeKey);
660
706
  const override = config.toolOverrides[name];
661
707
  const defaults = config.execution?.defaults[name];
@@ -689,9 +735,7 @@ function installPolicy(ctx, scopeKey, config) {
689
735
  for (const release of disposers.splice(0).reverse()) release();
690
736
  };
691
737
  try {
692
- disposers.push(ctx.tools.restrict({ allow: available }));
693
- const excess = ctx.tools.schemas(scopeKey).filter((tool) => !selected.has(tool.name));
694
- 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] }));
695
739
  disposers.push(ctx.tools.guard((execution) => selected.has(execution.name) ? void 0 : "Tool excluded by Studio preset policy"));
696
740
  for (const definition of definitions) disposers.push(ctx.tools.register(definition));
697
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 }));
@@ -758,6 +802,7 @@ function executionCapabilities(tools) {
758
802
  }
759
803
 
760
804
  // src/host/preset-store.ts
805
+ import { createScope as createScope2 } from "@deepseek-ai/dsh-scope";
761
806
  import { randomUUID as randomUUID3 } from "node:crypto";
762
807
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, rename as rename2 } from "node:fs/promises";
763
808
  import { dirname as dirname2, join } from "node:path";
@@ -787,11 +832,27 @@ function parseRoles(input) {
787
832
  if (!/^[a-zA-Z0-9_-]+$/.test(reasoning)) throw new Error("Role reasoning must be inherit or an effort identifier");
788
833
  if (spawn !== "fresh" && spawn !== "fork") throw new Error("Role spawn must be fresh or fork");
789
834
  seen.add(role);
790
- 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
+ };
791
847
  });
792
848
  }
793
849
  function executableRoles(roles) {
794
- 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])];
795
856
  }
796
857
 
797
858
  // src/host/role-validation.ts
@@ -821,7 +882,7 @@ async function validateRoleModel(llm, role, signal) {
821
882
 
822
883
  // src/host/preset-store.ts
823
884
  var owned = (id) => id === OPTIMIZED_PRESET_ID || /^studio-user-[a-f0-9-]{36}$/.test(id);
824
- 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.";
825
886
  function migrateLegacyNames(row) {
826
887
  const request = row.request;
827
888
  if (request.toolOverrides) {
@@ -864,6 +925,7 @@ var PresetStore = class {
864
925
  if (!owned(row.id)) throw new Error("Invalid Studio preset ownership");
865
926
  parsePreviewRequest(row.request);
866
927
  migrateLegacyNames(row);
928
+ if (row.applyWarning === retiredLegacyWarning) delete row.applyWarning;
867
929
  }
868
930
  } catch (error) {
869
931
  if (error.code !== "ENOENT") throw error;
@@ -879,10 +941,32 @@ var PresetStore = class {
879
941
  })();
880
942
  }
881
943
  async mount(id, definition) {
882
- 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 });
883
946
  try {
884
947
  const preset = await this.ctx.agentPresets.resolve(id);
885
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
+ }
886
970
  this.disposers.set(id, dispose);
887
971
  } catch (error) {
888
972
  await dispose();
@@ -909,27 +993,72 @@ var PresetStore = class {
909
993
  const { applied, ...value } = row;
910
994
  return structuredClone({ ...value, ...row.id === OPTIMIZED_PRESET_ID ? { template: true, nonDeletable: true } : {} });
911
995
  }
912
- /** 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
+ */
913
1008
  ensureOptimized(tools, plugins) {
914
1009
  return this.serial(async () => {
915
- const existing = this.rows.find((row2) => row2.id === OPTIMIZED_PRESET_ID);
916
- if (existing) return this.public(existing);
917
1010
  const template = optimizedTemplate(tools);
918
- const request = template.request;
919
- const delegates = request.tools.includes(ROLE_TOOL);
920
- const base = applyConfigOverrides(plugins, request.configOverrides ?? {});
921
- 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);
922
1013
  const now = (/* @__PURE__ */ new Date()).toISOString();
923
- 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 } : {} };
924
- 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
+ }
925
1048
  try {
926
- await this.persist([...this.rows, row]);
1049
+ await this.persist(this.rows.map((row) => row.id === existing.id ? refresh : row));
927
1050
  } catch (error) {
928
- await this.disposers.get(row.id)?.();
929
- 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
+ }
930
1059
  throw error;
931
1060
  }
932
- return this.public(row);
1061
+ return this.public(refresh);
933
1062
  });
934
1063
  }
935
1064
  list() {
@@ -974,21 +1103,25 @@ var PresetStore = class {
974
1103
  for (const role of roles) await validateRoleModel(this.ctx.get("llm"), role);
975
1104
  const delegates = tools.includes(ROLE_TOOL);
976
1105
  const keep = (entries) => Object.fromEntries(Object.entries(entries).filter(([name]) => tools.includes(name)));
977
- 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] } } : {} } }] : [];
978
1113
  const execution = request.execution && {
979
1114
  ...request.execution,
980
1115
  defaults: keep(request.execution.defaults),
981
1116
  ...request.execution.batch && !tools.includes(request.execution.batch.tool) ? { batch: void 0 } : {}
982
1117
  };
983
- 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 }) };
984
1119
  await this.disposers.get(id)?.();
985
1120
  this.disposers.delete(id);
986
1121
  try {
987
1122
  await this.mount(id, applied);
988
- 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"));
989
1123
  const row = { ...old, applied, appliedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "applied" };
990
- if (hasLegacySettings) row.applyWarning = warning;
991
- else delete row.applyWarning;
1124
+ delete row.applyWarning;
992
1125
  await this.persist(this.rows.map((item) => item.id === id ? row : item));
993
1126
  return this.public(row);
994
1127
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matamt-dbg/dsh-studio",
3
- "version": "0.2.2",
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,11 +130,20 @@ 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
+ }
85
136
  const missing = config.tools.filter((name2) => ctx.tools.get(name2, scopeKey) === void 0);
86
- if (missing.length) console.warn(`Studio policy: ${missing.length} selected tool(s) unavailable on this host, skipped: ${missing.join(", ")}`);
87
- const available = config.tools.filter((name2) => !missing.includes(name2));
88
- const selected = new Set(available);
89
- const definitions = available.map((name2) => {
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
+ }
145
+ const selected = new Set(config.tools);
146
+ const definitions = config.tools.map((name2) => {
90
147
  const definition = ctx.tools.get(name2, scopeKey);
91
148
  const override = config.toolOverrides[name2];
92
149
  const defaults = config.execution?.defaults[name2];
@@ -120,9 +177,7 @@ function installPolicy(ctx, scopeKey, config) {
120
177
  for (const release of disposers.splice(0).reverse()) release();
121
178
  };
122
179
  try {
123
- disposers.push(ctx.tools.restrict({ allow: available }));
124
- const excess = ctx.tools.schemas(scopeKey).filter((tool) => !selected.has(tool.name));
125
- 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] }));
126
181
  disposers.push(ctx.tools.guard((execution) => selected.has(execution.name) ? void 0 : "Tool excluded by Studio preset policy"));
127
182
  for (const definition of definitions) disposers.push(ctx.tools.register(definition));
128
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 }));
@@ -137,23 +192,91 @@ var name = "studio-preset-policy";
137
192
  var inject = ["tools", "systemPrompt"];
138
193
  function apply(ctx, config) {
139
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
+ };
140
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
+ };
141
218
  ctx.on("agent/created", ({ agent }) => {
142
- if (!installed.has(agent)) installed.set(agent, installPolicy(agent.ctx, agent, config));
219
+ install(agent);
143
220
  return void 0;
144
221
  });
145
222
  ctx.on("agent/disposed", ({ agent }) => {
146
- installed.get(agent)?.();
147
- 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);
148
268
  });
269
+ reconcile();
149
270
  ctx.effect(() => () => {
150
271
  for (const dispose of installed.values()) dispose();
151
272
  installed.clear();
273
+ for (const agent of [...blocked.keys()]) unblock(agent);
152
274
  });
153
275
  }
154
276
  export {
155
277
  POLICY_SECTION_ORDERS,
156
278
  apply,
279
+ childPolicy,
157
280
  inject,
158
281
  installPolicy,
159
282
  name,