@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/README.md +9 -4
- package/client.js +281 -174
- package/index.js +192 -57
- package/package.json +1 -1
- package/policy.js +134 -9
- 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
|
|
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
|
|
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) =>
|
|
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
|
|
578
|
+
const base = request.preset;
|
|
543
579
|
if (request.configOverrides && base !== MASTER_PRESET_ID) throw new Error("Config overrides require Studio Master");
|
|
544
|
-
const
|
|
545
|
-
|
|
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
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
917
|
-
|
|
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
|
|
922
|
-
|
|
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(
|
|
1049
|
+
await this.persist(this.rows.map((row) => row.id === existing.id ? refresh : row));
|
|
925
1050
|
} catch (error) {
|
|
926
|
-
await this.disposers.get(
|
|
927
|
-
this.disposers.delete(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
219
|
+
install(agent);
|
|
141
220
|
return void 0;
|
|
142
221
|
});
|
|
143
222
|
ctx.on("agent/disposed", ({ agent }) => {
|
|
144
|
-
|
|
145
|
-
|
|
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,
|