@morlay/dsh-session-mode 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -23,6 +23,8 @@ interface SessionMode {
23
23
  readonly persona: SessionModePersona;
24
24
  readonly allowTools: string[];
25
25
  readonly denyTools: string[];
26
+ readonly allowSkills: string[];
27
+ readonly denySkills: string[];
26
28
  readonly allowPolicies: string[];
27
29
  readonly denyPolicies: string[];
28
30
  readonly instructions: boolean;
package/dist/index.d.mts CHANGED
@@ -23,6 +23,8 @@ interface SessionMode {
23
23
  readonly persona: SessionModePersona;
24
24
  readonly allowTools: string[];
25
25
  readonly denyTools: string[];
26
+ readonly allowSkills: string[];
27
+ readonly denySkills: string[];
26
28
  readonly allowPolicies: string[];
27
29
  readonly denyPolicies: string[];
28
30
  readonly instructions: boolean;
package/dist/index.mjs CHANGED
@@ -54,6 +54,14 @@ const modeSchema = z$1.object({
54
54
  zh: "这个模式不用的工具(黑名单):从 `allowTools` 定下的那份里减掉(`allowTools` 留空就是从全部里减)。留空 = 一条都不禁;同时命中两份名单时以这里为准。",
55
55
  en: "Tools this mode must not use (deny list): subtracted from whatever `allowTools` settled on (with an empty `allowTools`, from everything). Empty denies nothing; a name in both lists is denied."
56
56
  })),
57
+ allowSkills: z$1.array(z$1.string()).default([]).description(localized({
58
+ zh: "这个模式能用的技能;其余既不进技能目录,`skill` 工具加载它也被拒。**留空就是不设收窄**:技能注册表里有什么就用什么。",
59
+ en: "Skills this mode may use; everything else leaves the skill catalog and loading it through the `skill` tool is refused. Leave it empty to narrow nothing: the session then sees every skill the registry carries."
60
+ })),
61
+ denySkills: z$1.array(z$1.string()).default([]).description(localized({
62
+ zh: "这个模式不用的技能(黑名单):从 `allowSkills` 定下的那份里减掉(`allowSkills` 留空就是从全部里减)。留空 = 一条都不禁;同时命中两份名单时以这里为准。",
63
+ en: "Skills this mode must not use (deny list): subtracted from whatever `allowSkills` settled on (with an empty `allowSkills`, from everything). Empty denies nothing; a name in both lists is denied."
64
+ })),
57
65
  allowPolicies: z$1.array(z$1.string()).default([]).description(localized({
58
66
  zh: "上游 policy 规则的生效白名单(`fs/write-intent` / `fs/edit-intent`):留空 = 全部规则照旧生效;有值 = 只有列出的生效,其余的被绕过。",
59
67
  en: "Allow list of upstream policy rules that stay in force (`fs/write-intent` / `fs/edit-intent`): empty keeps every rule; when set, only the listed ones stay, and the others are bypassed."
@@ -135,24 +143,73 @@ const DENYLISTED = (toolName, modeName) => `${toolName} 被「${modeName}」的
135
143
  function refusalOf(toolName, modeName, denial) {
136
144
  return denial === "denylisted" ? DENYLISTED(toolName, modeName) : OUT_OF_SCOPE(toolName, modeName);
137
145
  }
146
+ const SKILL_OUT_OF_SCOPE = (skillName, modeName) => `${skillName} 不在「${modeName}」的技能白名单里,加载它不会有结果;按当前模式可用的技能完成任务,或让用户切到别的模式。`;
147
+ const SKILL_DENYLISTED = (skillName, modeName) => `${skillName} 被「${modeName}」的技能黑名单排除,加载它不会有结果;按当前模式可用的技能完成任务,或让用户切到别的模式。`;
148
+ function skillRefusalOf(skillName, modeName, denial) {
149
+ return denial === "denylisted" ? SKILL_DENYLISTED(skillName, modeName) : SKILL_OUT_OF_SCOPE(skillName, modeName);
150
+ }
138
151
  const TOOL_SECTION_PREFIX = "tool:";
139
- function gateOf(definition) {
140
- const deny = new Set(definition.denyTools);
141
- const allow = definition.allowTools.length === 0 ? void 0 : new Set(definition.allowTools);
142
- if (allow === void 0 && deny.size === 0) return void 0;
152
+ const SKILL_TOOL_NAME = "skill";
153
+ function gateOfNames(allow, deny) {
154
+ const denied = new Set(deny);
155
+ const allowed = allow.length === 0 ? void 0 : new Set(allow);
156
+ if (allowed === void 0 && denied.size === 0) return void 0;
143
157
  return {
144
- allow,
145
- deny
158
+ allow: allowed,
159
+ deny: denied
146
160
  };
147
161
  }
148
- function denialOf(gate, toolName) {
149
- if (gate.deny.has(toolName)) return "denylisted";
150
- if (gate.allow !== void 0 && !gate.allow.has(toolName)) return "not-allowlisted";
162
+ function denialOf(gate, name) {
163
+ if (gate.deny.has(name)) return "denylisted";
164
+ if (gate.allow !== void 0 && !gate.allow.has(name)) return "not-allowlisted";
151
165
  }
152
166
  function kindOf(message) {
153
167
  const kind = message.source.kind;
154
168
  return typeof kind === "string" ? kind : void 0;
155
169
  }
170
+ const CATALOG_LINE = /^- `([^`]+)`: /u;
171
+ function narrowCatalog(message, gate, warn) {
172
+ const source = message.source;
173
+ if (source.kind !== "skill-catalog") return void 0;
174
+ const entries = source.entries;
175
+ const denied = new Set(entries.map((entry) => entry.name).filter((name) => denialOf(gate, name) !== void 0));
176
+ if (denied.size === 0) return void 0;
177
+ const [block, ...rest] = message.content;
178
+ if (block?.type !== "text" || rest.length > 0) {
179
+ warn(`skill catalog message is not one text block; denied skills stay listed: ${[...denied].join(", ")}`);
180
+ return;
181
+ }
182
+ const removed = /* @__PURE__ */ new Set();
183
+ const lines = block.text.split("\n").filter((line) => {
184
+ const match = CATALOG_LINE.exec(line);
185
+ if (match === null) return true;
186
+ const name = match[1] ?? "";
187
+ if (!denied.has(name)) return true;
188
+ removed.add(name);
189
+ return false;
190
+ });
191
+ const missing = [...denied].filter((name) => !removed.has(name));
192
+ if (missing.length > 0) {
193
+ warn(`skill catalog lines not recognized; denied skills stay listed: ${missing.join(", ")}`);
194
+ return;
195
+ }
196
+ return {
197
+ ...message,
198
+ content: [{
199
+ ...block,
200
+ text: lines.join("\n")
201
+ }],
202
+ source: {
203
+ ...source,
204
+ entries: entries.filter((entry) => !denied.has(entry.name))
205
+ }
206
+ };
207
+ }
208
+ function skillNameOf(arguments_) {
209
+ if (typeof arguments_ !== "object" || arguments_ === null) return void 0;
210
+ const name = arguments_.name;
211
+ return typeof name === "string" && name.length > 0 ? name : void 0;
212
+ }
156
213
  var SessionScope = class {
157
214
  applied = /* @__PURE__ */ new WeakMap();
158
215
  ctx;
@@ -175,30 +232,54 @@ var SessionScope = class {
175
232
  if (decision.kind !== "enter") return decision;
176
233
  const applied = this.applied.get(payload.agent);
177
234
  if (applied === void 0) return decision;
178
- const kept = decision.messages.filter((message) => {
235
+ let changed = false;
236
+ const kept = [];
237
+ for (const message of decision.messages) {
179
238
  const kind = kindOf(message);
180
- if (kind === "agent-instructions") return applied.instructions;
181
- if (kind === "skill-catalog") return applied.skills;
182
- return true;
183
- });
184
- return kept.length === decision.messages.length ? decision : {
239
+ if (kind === "agent-instructions") {
240
+ if (applied.instructions) kept.push(message);
241
+ else changed = true;
242
+ continue;
243
+ }
244
+ if (kind === "skill-catalog") {
245
+ if (!applied.skills) {
246
+ changed = true;
247
+ continue;
248
+ }
249
+ const narrowed = applied.skillGate === void 0 ? void 0 : narrowCatalog(message, applied.skillGate, (line) => this.ctx.logger.warn(`session-mode: ${line}`));
250
+ if (narrowed === void 0) kept.push(message);
251
+ else {
252
+ changed = true;
253
+ kept.push(narrowed);
254
+ }
255
+ continue;
256
+ }
257
+ kept.push(message);
258
+ }
259
+ return changed ? {
185
260
  ...decision,
186
261
  messages: kept
187
- };
262
+ } : decision;
188
263
  }, { prepend: true });
189
264
  }
190
265
  apply(agent, definition) {
191
266
  const previous = this.applied.get(agent);
192
267
  if (previous !== void 0) previous.dispose();
193
- const gate = gateOf(definition);
268
+ const gate = gateOfNames(definition.allowTools, definition.denyTools);
269
+ const skillGate = gateOfNames(definition.allowSkills, definition.denySkills);
194
270
  const dispose = agent.ctx.effect(() => {
195
271
  const stoppers = [];
196
272
  if (definition.runtimeContext === false) stoppers.push(agent.ctx.systemPrompt.suppressRuntimeContext());
197
273
  const fiber = agent.ctx.inject(["tools"], (scope) => {
198
- if (gate === void 0) return;
274
+ if (gate === void 0 && skillGate === void 0) return;
199
275
  scope.tools.guard((exec) => {
200
- const denial = denialOf(gate, exec.name);
201
- return denial === void 0 ? void 0 : refusalOf(exec.name, definition.name, denial);
276
+ const denial = gate === void 0 ? void 0 : denialOf(gate, exec.name);
277
+ if (denial !== void 0) return refusalOf(exec.name, definition.name, denial);
278
+ if (skillGate === void 0 || exec.name !== SKILL_TOOL_NAME) return void 0;
279
+ const skillName = skillNameOf(exec.arguments);
280
+ if (skillName === void 0) return void 0;
281
+ const skillDenial = denialOf(skillGate, skillName);
282
+ return skillDenial === void 0 ? void 0 : skillRefusalOf(skillName, definition.name, skillDenial);
202
283
  });
203
284
  });
204
285
  return [...stoppers, () => fiber.dispose()];
@@ -206,6 +287,7 @@ var SessionScope = class {
206
287
  this.channel()?.setInstructions(agent, definition.instructions);
207
288
  this.applied.set(agent, {
208
289
  gate,
290
+ skillGate,
209
291
  instructions: definition.instructions,
210
292
  skills: definition.skills,
211
293
  dispose
@@ -421,6 +503,8 @@ var SessionModes = class extends Service {
421
503
  name: mode.name,
422
504
  allowTools: mode.allowTools,
423
505
  denyTools: mode.denyTools,
506
+ allowSkills: mode.allowSkills,
507
+ denySkills: mode.denySkills,
424
508
  instructions: mode.instructions,
425
509
  skills: mode.skills ?? derivedSkills(mode),
426
510
  runtimeContext: mode.runtimeContext
package/dist/rows.mjs CHANGED
@@ -11,6 +11,12 @@ const MODE_SOURCES = [{
11
11
  description: "功能完整的编码 Agent:文件、Shell、检索、联网等工具常驻,其余用法说明按需加载。",
12
12
  persona: CODING_PERSONA,
13
13
  role: ["main", "subagent"],
14
+ denyTools: ["load_workspace_dependencies"],
15
+ denySkills: [
16
+ "office-docx",
17
+ "office-pptx",
18
+ "office-xlsx"
19
+ ],
14
20
  denyPolicies: ["fs/edit-intent"]
15
21
  }, {
16
22
  id: "chat",
@@ -34,6 +40,8 @@ function list(values) {
34
40
  function modeConfig(source) {
35
41
  const allowTools = list(source.allowTools);
36
42
  const denyTools = list(source.denyTools);
43
+ const allowSkills = list(source.allowSkills);
44
+ const denySkills = list(source.denySkills);
37
45
  const allowPolicies = list(source.allowPolicies);
38
46
  const denyPolicies = list(source.denyPolicies);
39
47
  return {
@@ -44,6 +52,8 @@ function modeConfig(source) {
44
52
  ...source.persona === void 0 ? {} : { persona: { ...source.persona } },
45
53
  ...allowTools === void 0 ? {} : { allowTools },
46
54
  ...denyTools === void 0 ? {} : { denyTools },
55
+ ...allowSkills === void 0 ? {} : { allowSkills },
56
+ ...denySkills === void 0 ? {} : { denySkills },
47
57
  ...allowPolicies === void 0 ? {} : { allowPolicies },
48
58
  ...denyPolicies === void 0 ? {} : { denyPolicies },
49
59
  ...source.instructions === void 0 ? {} : { instructions: source.instructions },
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@morlay/dsh-session-mode",
3
- "version": "0.0.4",
4
- "description": "Personal dsh bundle: session modes (coding / chat) as data — a mode is a persona plus its tool lists, its upstream policy lists, its instruction / skill-catalog / runtime-context switches and an optional default model. The host applies the current mode to each session, and the chip in the composer sits beside the official agent-preset entry, which stays.",
3
+ "version": "0.0.6",
4
+ "description": "Session modes for dsh: coding / chat as data — a mode is a persona plus its tool lists, its upstream policy lists, its instruction / skill-catalog / runtime-context switches and an optional default model. The host applies the current mode to each session, and the chip in the composer sits beside the official agent-preset entry, which stays.",
5
5
  "keywords": [
6
6
  "dsh",
7
- "dsh-bundle",
7
+ "dsh-plugin",
8
8
  "persona",
9
9
  "session-mode"
10
10
  ],
@@ -41,27 +41,28 @@
41
41
  "@deepseek-ai/cordis-plugin-include": "1.0.9",
42
42
  "@deepseek-ai/cordis-plugin-loader": "1.0.5",
43
43
  "@deepseek-ai/cosmokit": "1.8.5",
44
- "@deepseek-ai/dsh-agent": "0.2.0-rc.1",
45
- "@deepseek-ai/dsh-agent-loop-testkit": "0.2.0-rc.1",
46
- "@deepseek-ai/dsh-agent-preset-registry": "0.2.0-rc.1",
47
- "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.1",
48
- "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.1",
49
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
50
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.1",
51
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.1",
52
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
53
- "@deepseek-ai/dsh-fs": "0.2.0-rc.1",
54
- "@deepseek-ai/dsh-fs-local": "0.2.0-rc.1",
55
- "@deepseek-ai/dsh-fs-observation-policy": "0.2.0-rc.1",
56
- "@deepseek-ai/dsh-llm": "0.2.0-rc.1",
57
- "@deepseek-ai/dsh-session": "0.2.0-rc.1",
58
- "@deepseek-ai/dsh-session-projection": "0.2.0-rc.1",
59
- "@deepseek-ai/dsh-skill": "0.2.0-rc.1",
60
- "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.1",
61
- "@deepseek-ai/dsh-tools": "0.2.0-rc.1",
62
- "@morlay/dsh-client-ui-conversation": "0.1.3",
63
- "@morlay/dsh-client-ui-primitives": "0.0.6",
64
- "@morlay/dsh-context-assembler": "0.0.4",
44
+ "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
45
+ "@deepseek-ai/dsh-agent-loop-testkit": "0.2.0-rc.2",
46
+ "@deepseek-ai/dsh-agent-preset-registry": "0.2.0-rc.2",
47
+ "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2",
48
+ "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
49
+ "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
50
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
51
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
52
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
53
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
54
+ "@deepseek-ai/dsh-fs": "0.2.0-rc.2",
55
+ "@deepseek-ai/dsh-fs-local": "0.2.0-rc.2",
56
+ "@deepseek-ai/dsh-fs-observation-policy": "0.2.0-rc.2",
57
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
58
+ "@deepseek-ai/dsh-session": "0.2.0-rc.2",
59
+ "@deepseek-ai/dsh-session-projection": "0.2.0-rc.2",
60
+ "@deepseek-ai/dsh-skill": "0.2.0-rc.2",
61
+ "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
62
+ "@deepseek-ai/dsh-tool-skill": "0.2.0-rc.2",
63
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
64
+ "@morlay/dsh-client-ui-primitives": "0.0.7",
65
+ "@morlay/dsh-context-assembler": "0.0.5",
65
66
  "@testing-library/react": "^16.3.0",
66
67
  "@types/js-yaml": "^4.0.9",
67
68
  "@types/react": "~18.3.1",
@@ -72,28 +73,26 @@
72
73
  "peerDependencies": {
73
74
  "@deepseek-ai/cordis": "^4.0.4",
74
75
  "@deepseek-ai/cosmokit": "^1.8.5",
75
- "@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
76
- "@deepseek-ai/dsh-agent-preset-registry": "^0.2.0-rc.1",
77
- "@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.1",
78
- "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.1",
79
- "@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.1",
80
- "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1",
81
- "@deepseek-ai/dsh-fs": "^0.2.0-rc.1",
82
- "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
83
- "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
84
- "@deepseek-ai/dsh-session-projection": "^0.2.0-rc.1",
85
- "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.1",
86
- "@deepseek-ai/schemastery": "^3.18.4",
87
- "@morlay/dsh-client-ui-conversation": "0.1.3",
88
- "@morlay/dsh-client-ui-primitives": "0.0.6"
76
+ "@deepseek-ai/dsh-agent": "^0.2.0-rc.2",
77
+ "@deepseek-ai/dsh-agent-preset-registry": "^0.2.0-rc.2",
78
+ "@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.2",
79
+ "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
80
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.2.0-rc.2",
81
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.2",
82
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
83
+ "@deepseek-ai/dsh-fs": "^0.2.0-rc.2",
84
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
85
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.2",
86
+ "@deepseek-ai/dsh-session-projection": "^0.2.0-rc.2",
87
+ "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
88
+ "@deepseek-ai/schemastery": "^3.18.4"
89
89
  },
90
90
  "dsh": {
91
91
  "client": {
92
92
  "platform": "web",
93
93
  "inject": [
94
94
  "@deepseek-ai/dsh-client-locale",
95
- "@deepseek-ai/dsh-client-ui-slots",
96
- "@morlay/dsh-client-ui-primitives"
95
+ "@deepseek-ai/dsh-client-ui-slots"
97
96
  ]
98
97
  }
99
98
  },
@@ -12,7 +12,7 @@ import type { PropsLocale, PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots
12
12
  // Type-only:拉入本包 host 半的 Context / 投影声明(`sessionMode`),让 `projectionValues` 有类型。
13
13
  import type {} from "../index.ts";
14
14
  // Type-only:拉入 ui-conversation 的 SlotMap 合并(composer 工具行的座位)。
15
- import type {} from "@morlay/dsh-client-ui-conversation/client";
15
+ import type {} from "@deepseek-ai/dsh-client-ui-conversation/client";
16
16
  import { selectMode } from "./api.ts";
17
17
  import { useRoster } from "./use-roster.ts";
18
18
  import css from "./SessionModeSeat.module.css";
@@ -8,7 +8,8 @@ import type {} from "@deepseek-ai/dsh-api-remotes/client";
8
8
  import type {} from "@deepseek-ai/dsh-client-locale/client";
9
9
  import type {} from "@deepseek-ai/dsh-client-ui-slots";
10
10
  // Type-only:槽位声明与 standard props(session / session-maybe / global)。
11
- import type {} from "@morlay/dsh-client-ui-conversation/client";
11
+ import type {} from "@deepseek-ai/dsh-client-ui-conversation/client";
12
+ import { apply as installUiPrimitives } from "@morlay/dsh-client-ui-primitives/client";
12
13
  import { POLICY_NAMES } from "../shared.ts";
13
14
  import { SessionModeSeat } from "./SessionModeSeat.tsx";
14
15
  import { en, zh, type SessionModeLocaleKey } from "./locales.ts";
@@ -62,6 +63,8 @@ function readAt(root: unknown, path: readonly string[]): unknown {
62
63
 
63
64
  // 装上会话里的那一个面,以及本行配置页的字段文案。
64
65
  export function apply(ctx: Context): void {
66
+ // 基础面随本包 inline(不再是装配行):装上它提供的字典、字段槽与按行配置页;多份副本只装一次。
67
+ installUiPrimitives(ctx);
65
68
  const t = ctx.locale.bind(NS);
66
69
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "session-mode: dictionaries");
67
70
 
@@ -0,0 +1,6 @@
1
+ // 本包 client 半的组件样式是上游那套 CSS Modules(`SessionModeSeat.module.css`):bundler 解析得动,
2
+ // TS 需要这份 ambient 声明把它定成「有一个 class 映射的模块」。
3
+ declare module "*.module.css" {
4
+ const classes: Readonly<Record<string, string>>;
5
+ export default classes;
6
+ }
package/src/index.ts CHANGED
@@ -389,12 +389,14 @@ export class SessionModes extends Service {
389
389
  for (const dispose of disposers) dispose();
390
390
  },
391
391
  });
392
- // 收口要的那几项按模式定义整份推过去:`name` / `allowTools` / `denyTools` / 三个开关。`skills` 缺省时在这里
392
+ // 收口要的那几项按模式定义整份推过去:`name` / 两份工具名单 / 两份技能名单 / 三个开关。`skills` 缺省时在这里
393
393
  // 按这份定义自己的工具名单推导(见 `derivedSkills`)——收口那一侧只认解析后的布尔。
394
394
  this.scope.apply(agent, {
395
395
  name: mode.name,
396
396
  allowTools: mode.allowTools,
397
397
  denyTools: mode.denyTools,
398
+ allowSkills: mode.allowSkills,
399
+ denySkills: mode.denySkills,
398
400
  instructions: mode.instructions,
399
401
  skills: mode.skills ?? derivedSkills(mode),
400
402
  runtimeContext: mode.runtimeContext,
@@ -20,6 +20,10 @@ export interface ModeSource {
20
20
  readonly allowTools?: readonly string[];
21
21
  // 这个模式不用的工具(黑名单):从 `allowTools` 定的那份里减掉(deny 优先)。
22
22
  readonly denyTools?: readonly string[];
23
+ // 这个模式能用的技能;**可选**,不写(留空)就是不设收窄——技能注册表里有什么就用什么。
24
+ readonly allowSkills?: readonly string[];
25
+ // 这个模式不用的技能(黑名单):从 `allowSkills` 定的那份里减掉(deny 优先)。
26
+ readonly denySkills?: readonly string[];
23
27
  // 上游 policy 规则的生效白名单:**可选**,不写(留空)= 全部规则生效。
24
28
  readonly allowPolicies?: readonly PolicyName[];
25
29
  // 上游 policy 规则的黑名单:列出的规则禁用(它在上游那条 waterfall 上的裁决被绕过)。
@@ -69,7 +73,11 @@ export const MODE_SOURCES: readonly ModeSource[] = [
69
73
  // 用户可选,也允许作为子代理的 mode(子代理默认继承父 mode,不看角色;这里是"可被指定"的候选集)。
70
74
  role: ["main", "subagent"],
71
75
  // 不写 `allowTools`:不设收窄——这个会话用它挂着的 preset 提供的全部工具(抄一份清单只会与行清单漂移)。
72
- // 不写 `denyTools`:一件工具都不禁。
76
+ // 只排除一件工具:官方 Office 组合(宿主代码挂载,profile 层停不掉它的行)里那个载荷查询。
77
+ denyTools: ["load_workspace_dependencies"],
78
+ // 官方 Office 技能的三个名字同样按会话排除:技能目录里不列它们,`skill` 工具加载它们也被拒。
79
+ // 名单是技能名这一层的模型可见契约(官方 `skill-office` 的 `SKILL_NAMES`),跟着上游改名走。
80
+ denySkills: ["office-docx", "office-pptx", "office-xlsx"],
73
81
  //
74
82
  // `denyPolicies` 只禁 `fs/edit-intent`(上游那条"先读后改"):改文件不再要求先读过——写路径上的
75
83
  // `fs/write-intent`(陈旧版本 CAS 那层安全网)照旧生效,那正是这条配置不写成"两条都禁"的理由。
package/src/modes.ts CHANGED
@@ -51,6 +51,12 @@ export interface SessionMode {
51
51
  // 这个模式**不用**哪些工具(黑名单):从 `allowTools` 定下的那份里减掉(`allowTools` 留空时就是从全部里减)。
52
52
  // 留空表示不禁任何工具。同时命中两份名单时以这里为准(deny 优先)。
53
53
  readonly denyTools: string[];
54
+ // 这个模式能用哪些**技能**;其余的既不进技能目录,`skill` 工具加载它也被拒。**留空表示不设收窄**:技能注册表
55
+ // 里有什么就用什么(官方 `skill-office` 那类由宿主代码挂载的技能也在这份"全部"里)。
56
+ readonly allowSkills: string[];
57
+ // 这个模式**不用**哪些技能(黑名单):从 `allowSkills` 定下的那份里减掉(`allowSkills` 留空时就是从全部里减)。
58
+ // 留空表示不禁任何技能;同时命中两份名单时以这里为准(deny 优先)。
59
+ readonly denySkills: string[];
54
60
  // 上游 policy 规则的**生效白名单**:留空(或不写)表示全部规则照旧生效;有值表示只有列出的那些生效,
55
61
  // 其余的按 `denyPolicies` 那一套被绕过。名单见 `POLICY_NAMES`。空数组与不写同义。
56
62
  readonly allowPolicies: string[];
@@ -198,6 +204,24 @@ const modeSchema: z<SessionMode> = z.object({
198
204
  en: "Tools this mode must not use (deny list): subtracted from whatever `allowTools` settled on (with an empty `allowTools`, from everything). Empty denies nothing; a name in both lists is denied.",
199
205
  }),
200
206
  ),
207
+ allowSkills: z
208
+ .array(z.string())
209
+ .default([])
210
+ .description(
211
+ localized({
212
+ zh: "这个模式能用的技能;其余既不进技能目录,`skill` 工具加载它也被拒。**留空就是不设收窄**:技能注册表里有什么就用什么。",
213
+ en: "Skills this mode may use; everything else leaves the skill catalog and loading it through the `skill` tool is refused. Leave it empty to narrow nothing: the session then sees every skill the registry carries.",
214
+ }),
215
+ ),
216
+ denySkills: z
217
+ .array(z.string())
218
+ .default([])
219
+ .description(
220
+ localized({
221
+ zh: "这个模式不用的技能(黑名单):从 `allowSkills` 定下的那份里减掉(`allowSkills` 留空就是从全部里减)。留空 = 一条都不禁;同时命中两份名单时以这里为准。",
222
+ en: "Skills this mode must not use (deny list): subtracted from whatever `allowSkills` settled on (with an empty `allowSkills`, from everything). Empty denies nothing; a name in both lists is denied.",
223
+ }),
224
+ ),
201
225
  allowPolicies: z
202
226
  .array(z.string())
203
227
  .default([])
@@ -298,6 +322,10 @@ interface Validated {
298
322
  readonly allowTools?: readonly string[];
299
323
  // 留空合法:不禁任何工具;与 `allowTools` 同时命中合法(deny 优先)。
300
324
  readonly denyTools?: readonly string[];
325
+ // 留空合法:不设技能收窄。
326
+ readonly allowSkills?: readonly string[];
327
+ // 留空合法:不禁任何技能;与 `allowSkills` 同时命中合法(deny 优先)。
328
+ readonly denySkills?: readonly string[];
301
329
  // 留空合法:全部规则生效。
302
330
  readonly allowPolicies?: readonly string[];
303
331
  // 留空合法:一条都不禁;与 `allowPolicies` 同时命中合法(deny 优先)。
package/src/rows.ts CHANGED
@@ -24,6 +24,8 @@ function list<T>(values: readonly T[] | undefined): readonly T[] | undefined {
24
24
  function modeConfig(source: ModeSource): Record<string, unknown> {
25
25
  const allowTools = list(source.allowTools);
26
26
  const denyTools = list(source.denyTools);
27
+ const allowSkills = list(source.allowSkills);
28
+ const denySkills = list(source.denySkills);
27
29
  const allowPolicies = list(source.allowPolicies);
28
30
  const denyPolicies = list(source.denyPolicies);
29
31
  return {
@@ -35,9 +37,12 @@ function modeConfig(source: ModeSource): Record<string, unknown> {
35
37
  ...(source.persona === undefined ? {} : { persona: { ...source.persona } }),
36
38
  // 四份名单都是可选的:不写(或写成空数组)就是这个键不进 config,schema 默认空数组。
37
39
  // `allowTools` 不写 = 不设收窄;`denyTools` 不写 = 一件都不禁(两份同配时 deny 优先);
40
+ // `allowSkills` / `denySkills` 同一套语义,收的是技能名(技能目录与 `skill` 工具两处一起收);
38
41
  // `allowPolicies` 不写 = 全部上游 policy 规则生效;`denyPolicies` 不写 = 一条都不禁。
39
42
  ...(allowTools === undefined ? {} : { allowTools }),
40
43
  ...(denyTools === undefined ? {} : { denyTools }),
44
+ ...(allowSkills === undefined ? {} : { allowSkills }),
45
+ ...(denySkills === undefined ? {} : { denySkills }),
41
46
  ...(allowPolicies === undefined ? {} : { allowPolicies }),
42
47
  ...(denyPolicies === undefined ? {} : { denyPolicies }),
43
48
  ...(source.instructions === undefined ? {} : { instructions: source.instructions }),