@matamt-dbg/dsh-studio 0.2.2 → 0.2.8

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 +499 -219
  3. package/index.js +340 -64
  4. package/package.json +1 -1
  5. package/policy.js +135 -12
  6. package/roles.js +25 -3
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,
package/roles.js CHANGED
@@ -50,11 +50,27 @@ function parseRoles(input) {
50
50
  if (!/^[a-zA-Z0-9_-]+$/.test(reasoning)) throw new Error("Role reasoning must be inherit or an effort identifier");
51
51
  if (spawn !== "fresh" && spawn !== "fork") throw new Error("Role spawn must be fresh or fork");
52
52
  seen.add(role);
53
- return { name: name2, role, model, reasoning, spawn, persona, ..."overrides" in value ? { overrides: structuredClone(value.overrides) } : {} };
53
+ const filter = value.toolFilter;
54
+ if (filter !== void 0 && (!object(filter) || !Array.isArray(filter.deny) || filter.deny.some((name3) => typeof name3 !== "string" || !name3) || filter.mode !== void 0 && filter.mode !== "extend" && filter.mode !== "replace")) throw new Error("Invalid role tool filter");
55
+ return {
56
+ name: name2,
57
+ role,
58
+ model,
59
+ reasoning,
60
+ spawn,
61
+ persona,
62
+ ...filter !== void 0 ? { toolFilter: { mode: filter.mode ?? "replace", deny: [...new Set(filter.deny)] } } : {},
63
+ ..."overrides" in value ? { overrides: structuredClone(value.overrides) } : {}
64
+ };
54
65
  });
55
66
  }
56
67
  function executableRoles(roles) {
57
- return roles.map(({ name: name2, role, model, reasoning, spawn, persona }) => ({ name: name2, role, model, reasoning, spawn, persona }));
68
+ return roles.map(({ name: name2, role, model, reasoning, spawn, persona, toolFilter }) => ({ name: name2, role, model, reasoning, spawn, persona, ...toolFilter ? { toolFilter } : {} }));
69
+ }
70
+ function roleToolFilter(role, presetDefault = []) {
71
+ const own = role?.toolFilter;
72
+ if (!own) return [...presetDefault];
73
+ return own.mode === "replace" ? [...own.deny] : [.../* @__PURE__ */ new Set([...presetDefault, ...own.deny])];
58
74
  }
59
75
 
60
76
  // src/package-name.ts
@@ -72,6 +88,9 @@ var inject = ["tools", "subagents", "llm"];
72
88
  var PARENT_ROLE = { name: "Subagent", role: "subagent", model: "inherit", reasoning: "inherit", spawn: "fresh", persona: "" };
73
89
  function apply(ctx, config) {
74
90
  const roles = executableRoles(parseRoles(config.roles));
91
+ const deny = config.toolFilter?.deny;
92
+ if (deny !== void 0 && (!Array.isArray(deny) || deny.some((name2) => typeof name2 !== "string" || !name2))) throw new Error("Invalid subagent tool filter");
93
+ const presetDefault = deny ?? [];
75
94
  ctx.tools.register(defineTool({
76
95
  name: ROLE_TOOL,
77
96
  description: roles.length ? "Discover saved specialist roles or delegate a task to one. Calls return a native child agent id; the child reports back through normal subagent delivery." : "Delegate a self-contained task to a subagent that starts with the same model and settings as you. Calls return a native child agent id; the child reports back through normal subagent delivery.",
@@ -102,6 +121,8 @@ function apply(ctx, config) {
102
121
  }
103
122
  if (role.reasoning !== "inherit") agentOptions.reasoningEffort = ReasoningEffortId2(role.reasoning);
104
123
  const maxDepth = ctx.subagents.resolveMaxDepth();
124
+ const hidden = roleToolFilter(roles.length ? role : void 0, presetDefault);
125
+ const toolFilter = hidden.length ? { deny: hidden } : void 0;
105
126
  const spec = {
106
127
  provider: role.spawn === "fork" ? "fork" : "spawn",
107
128
  label: role.name,
@@ -110,7 +131,8 @@ function apply(ctx, config) {
110
131
  prompt: [{ type: "text", text: args.task }],
111
132
  ...Object.keys(agentOptions).length ? { agentOptions } : {},
112
133
  ...role.persona ? { persona: role.persona } : {},
113
- ...maxDepth === void 0 ? {} : { maxDepth }
134
+ ...maxDepth === void 0 ? {} : { maxDepth },
135
+ ...toolFilter ? { toolFilter } : {}
114
136
  },
115
137
  signal: exec.signal
116
138
  };